From f91ee97502404d476672d1199a320e99be0d00fb Mon Sep 17 00:00:00 2001 From: john Date: Fri, 28 Aug 2026 09:39:06 +0800 Subject: [PATCH] feat(wechat): send rich subscribe welcome with clickable intro and bind links New followers receive capability overview plus WeChat anchor links for the deep intro page and account binding instead of exposing raw URLs. Co-authored-by: Cursor --- wechat-mp-config.mjs | 4 +++ wechat-mp.mjs | 6 +++- wechat/handlers/sync-replies.mjs | 37 ++++++++++++++++++++++ wechat/handlers/sync-replies.test.mjs | 45 +++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 wechat/handlers/sync-replies.test.mjs diff --git a/wechat-mp-config.mjs b/wechat-mp-config.mjs index 6e1c43b..c630e9f 100644 --- a/wechat-mp-config.mjs +++ b/wechat-mp-config.mjs @@ -4,6 +4,8 @@ const DEFAULT_STATUS_TEXT = '我在这边。上一条如果还没完成,我会继续把结果发给你;你也可以直接补一句要求。'; const DEFAULT_UNSUPPORTED_TEXT = '当前先支持文字消息,你可以直接发文字给我。'; const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:'; +const DEFAULT_SUBSCRIBE_INTRO_URL = + 'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/tkmind-deep-intro.html'; const DEFAULT_PROGRESS_DELAY_MS = 8000; const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000; const DEFAULT_SESSION_MESSAGE_ROTATE_COUNT = 200; @@ -69,6 +71,8 @@ export function loadWechatMpConfig(env = process.env) { statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT, unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT, unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT, + subscribeIntroUrl: + env.H5_WECHAT_MP_SUBSCRIBE_INTRO_URL?.trim() || DEFAULT_SUBSCRIBE_INTRO_URL, tokenUrl: env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_WECHAT_TOKEN_URL, customerServiceUrl: env.H5_WECHAT_MP_CUSTOMER_SERVICE_URL?.trim() || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL, diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 039a8ca..89b1f23 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -42,6 +42,7 @@ import { handleWechatIntentTransaction } from './wechat/handlers/intent-transact import { handleWechatScheduledTaskIntent } from './wechat/handlers/scheduled-task.mjs'; import { buildStatusText, + buildSubscribeWelcomeText, resolveSyncReply, shouldHandleSyncReply, } from './wechat/handlers/sync-replies.mjs'; @@ -3768,7 +3769,10 @@ export function createWechatMpService({ body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, - content: `${config.unboundTextPrefix}\n${buildBindUrl()}`, + content: buildSubscribeWelcomeText({ + bindUrl: buildBindUrl(), + introUrl: config.subscribeIntroUrl, + }), }), }; } diff --git a/wechat/handlers/sync-replies.mjs b/wechat/handlers/sync-replies.mjs index 92b2a6d..bc73c2b 100644 --- a/wechat/handlers/sync-replies.mjs +++ b/wechat/handlers/sync-replies.mjs @@ -1,5 +1,42 @@ import { resolveWechatAddressName } from '../user/display-name.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 function buildWechatTextLink(href, label) { + const normalizedHref = String(href ?? '').trim(); + const normalizedLabel = String(label ?? '').trim(); + if (!normalizedHref) return normalizedLabel; + if (!normalizedLabel) return normalizedHref; + return `${normalizedLabel}`; +} + +export function buildSubscribeWelcomeText({ bindUrl, introUrl = DEFAULT_SUBSCRIBE_INTRO_URL } = {}) { + const normalizedBindUrl = String(bindUrl ?? '').trim(); + const normalizedIntroUrl = String(introUrl ?? '').trim(); + const lines = [ + '欢迎关注 TKMind 智趣 👋', + '', + '在微信里直接发消息即可:', + '· 问答、写作、翻译', + '· 生成精美网页并给链接', + '· 发图解读报告/截图', + '· 设置定时提醒与每日推送', + ]; + if (normalizedIntroUrl) { + lines.push('', '📖 了解更多:', 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}!我在呢,有什么需要?` : '你好!我在呢,有什么需要?'; diff --git a/wechat/handlers/sync-replies.test.mjs b/wechat/handlers/sync-replies.test.mjs new file mode 100644 index 0000000..2dd1f59 --- /dev/null +++ b/wechat/handlers/sync-replies.test.mjs @@ -0,0 +1,45 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + buildSubscribeWelcomeText, + buildWechatTextLink, + DEFAULT_PRODUCTION_BIND_URL, + DEFAULT_SUBSCRIBE_INTRO_URL, +} from './sync-replies.mjs'; + +test('buildWechatTextLink renders WeChat anchor without exposing raw url', () => { + assert.equal( + buildWechatTextLink(DEFAULT_PRODUCTION_BIND_URL, '点我绑定'), + `点我绑定`, + ); +}); + +test('buildSubscribeWelcomeText uses clickable anchors instead of raw urls', () => { + const text = buildSubscribeWelcomeText({ bindUrl: DEFAULT_PRODUCTION_BIND_URL }); + + assert.match(text, /欢迎关注 TKMind 智趣/); + assert.match(text, /生成精美网页并给链接/); + assert.match(text, /📖 了解更多:/); + assert.match( + text, + /点击链接<\/a>/, + ); + assert.match(text, /👉 先完成绑定再开始对话:/); + assert.match( + text, + /点我绑定<\/a>/, + ); + assert.doesNotMatch(text, /\nhttps:\/\/m\.tkmind\.cn/); + assert.match(text, /帮我做一个读书笔记页面/); +}); + +test('buildSubscribeWelcomeText omits intro section when introUrl is empty', () => { + const text = buildSubscribeWelcomeText({ + bindUrl: 'https://m.tkmind.cn/bind', + introUrl: '', + }); + + assert.doesNotMatch(text, /📖 了解更多:/); + assert.doesNotMatch(text, /tkmind-deep-intro\.html/); +});