From bb6aa75b3bf8ec8e3fde7f51d2bd3b57919ed85e Mon Sep 17 00:00:00 2001 From: john Date: Sun, 20 Sep 2026 19:10:27 +0800 Subject: [PATCH] feat(wechat): add secret codes for morning subscribe and news push Co-authored-by: Cursor --- docs/branch-disposition.md | 24 ++++++++++++ wechat-mp.mjs | 3 ++ wechat/handlers/push-subscription.mjs | 34 +++++++++++++++++ wechat/handlers/push-subscription.test.mjs | 37 +++++++++++++++++++ wechat/push-subscription-secret-codes.mjs | 26 +++++++++++++ .../push-subscription-secret-codes.test.mjs | 24 ++++++++++++ 6 files changed, 148 insertions(+) create mode 100644 wechat/push-subscription-secret-codes.mjs create mode 100644 wechat/push-subscription-secret-codes.test.mjs diff --git a/docs/branch-disposition.md b/docs/branch-disposition.md index b397cba..3696b9f 100644 --- a/docs/branch-disposition.md +++ b/docs/branch-disposition.md @@ -1,5 +1,29 @@ # 历史分支处置登记 +## `feature/wechat-push-subscription-release` + +**状态:禁止再次引用。已 fast-forward 并入 `origin/main` 并完成 103 Portal 快速发布。** + +审计日期:2026-09-20 +分支 HEAD:`830f8a40` +`origin/main` 对应提交:`830f8a40` + +### 交付内容 + +- 微信服务号 10 项数字订阅闭环(开通/改时间/取消/查状态/到点推送) +- 早报封面生图链路扩展 + `scripts/push-wechat-news-morning-draft.mjs` 一键推草稿 +- 订阅推送与早报 Worker 同批上线 + +### 103 发布记录 + +- Portal runtime:`20260920-162911-830f8a40`(快速发布,`STUDIO_HOST=10.10.0.2`) +- full_backup:`/Users/john/Project/backups/memind/memind-full-20260920-162911-830f8a40-before.tar.gz` +- persist_backup:`/Users/john/Project/backups/memind/memind-persisted-20260920-162911-830f8a40-before.tar.gz` +- archived_source:`/Users/john/Project/archives/Memind-source-before-20260920-162911-830f8a40` +- 发布后:Portal/goosed 全 200;`/auth/login` 401;日志无 error keywords + +--- + ## `feature/chat-intent-stage-a` **状态:禁止再次引用。改动已提交并进入 `main`,该分支保留仅用于只读追溯,不是待合并开发分支。** diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 5fc4755..f1df73f 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -3967,6 +3967,9 @@ export function createWechatMpService({ sourceMessageId: inbound.msgId ?? null, wechatSubscribeMorningLlmConfigService, llmProviderService, + mysqlPool, + h5Root, + env, logger, }).catch((err) => { logger.warn?.( diff --git a/wechat/handlers/push-subscription.mjs b/wechat/handlers/push-subscription.mjs index 444c87c..402090a 100644 --- a/wechat/handlers/push-subscription.mjs +++ b/wechat/handlers/push-subscription.mjs @@ -32,6 +32,8 @@ import { 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'; async function tryCommitSubscription({ userId, @@ -238,12 +240,44 @@ export async function handlePushSubscriptionMessages({ 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') { + 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 '绑定账号后可查看订阅状态。'; diff --git a/wechat/handlers/push-subscription.test.mjs b/wechat/handlers/push-subscription.test.mjs index bd1152c..324f839 100644 --- a/wechat/handlers/push-subscription.test.mjs +++ b/wechat/handlers/push-subscription.test.mjs @@ -154,3 +154,40 @@ test('handlePushSubscriptionMessages returns help menu', async () => { assert.match(reply, /每日推送订阅/); assert.match(reply, /10 🎁 每日惊喜/); }); + +test('handlePushSubscriptionMessages opens morning subscription on secret code 美丽心情', async () => { + const calls = []; + const reply = await handlePushSubscriptionMessages({ + text: '美丽心情', + boundUser: { userId: 'user-1' }, + scheduleService: { + async listItems() { + return []; + }, + async findActiveItemByMetadataSource() { + return null; + }, + async createItem(payload) { + calls.push(['createItem', payload]); + return { id: 'item-morning', ...payload }; + }, + async createReminder(payload) { + calls.push(['createReminder', payload]); + return { id: 'rem-morning', ...payload }; + }, + }, + }); + + assert.match(reply, /已开启 ☀️ 早安问候/u); + assert.equal(calls[0][1].metadata.source, 'wechat_push:morning'); + assert.equal(calls[0][1].metadata.dailyHour, 8); +}); + +test('handlePushSubscriptionMessages pushes news digest on secret code 新闻早报', async () => { + const reply = await handlePushSubscriptionMessages({ + text: '新闻早报', + env: {}, + }); + + assert.match(reply, /📰 每日新闻早报/u); +}); diff --git a/wechat/push-subscription-secret-codes.mjs b/wechat/push-subscription-secret-codes.mjs new file mode 100644 index 0000000..0d83526 --- /dev/null +++ b/wechat/push-subscription-secret-codes.mjs @@ -0,0 +1,26 @@ +/** 微信服务号订阅暗号(精确匹配,忽略首尾与中间空白)。 */ + +export const PUSH_SUBSCRIPTION_SECRET_CODES = Object.freeze({ + morningSubscribe: '美丽心情', + newsImmediate: '新闻早报', +}); + +function normalizeSecretCodeText(text) { + return String(text ?? '').trim().replace(/\s+/g, ''); +} + +/** + * 解析订阅暗号。 + * @returns {{ kind: 'morning_subscribe' }|{ kind: 'news_immediate' }|null} + */ +export function parsePushSubscriptionSecretCode(text) { + const compact = normalizeSecretCodeText(text); + if (!compact) return null; + if (compact === PUSH_SUBSCRIPTION_SECRET_CODES.morningSubscribe) { + return { kind: 'morning_subscribe' }; + } + if (compact === PUSH_SUBSCRIPTION_SECRET_CODES.newsImmediate) { + return { kind: 'news_immediate' }; + } + return null; +} diff --git a/wechat/push-subscription-secret-codes.test.mjs b/wechat/push-subscription-secret-codes.test.mjs new file mode 100644 index 0000000..ce4493e --- /dev/null +++ b/wechat/push-subscription-secret-codes.test.mjs @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + parsePushSubscriptionSecretCode, + PUSH_SUBSCRIPTION_SECRET_CODES, +} from './push-subscription-secret-codes.mjs'; + +test('parsePushSubscriptionSecretCode matches morning subscribe code', () => { + assert.deepEqual(parsePushSubscriptionSecretCode('美丽心情'), { kind: 'morning_subscribe' }); + assert.deepEqual(parsePushSubscriptionSecretCode(' 美丽 心情 '), { kind: 'morning_subscribe' }); +}); + +test('parsePushSubscriptionSecretCode matches news immediate code', () => { + assert.deepEqual(parsePushSubscriptionSecretCode('新闻早报'), { kind: 'news_immediate' }); + assert.deepEqual(parsePushSubscriptionSecretCode(' 新闻 早报 '), { kind: 'news_immediate' }); +}); + +test('parsePushSubscriptionSecretCode ignores partial or unrelated text', () => { + assert.equal(parsePushSubscriptionSecretCode('美丽'), null); + assert.equal(parsePushSubscriptionSecretCode('2'), null); + assert.equal(parsePushSubscriptionSecretCode('取消新闻早报'), null); + assert.equal(parsePushSubscriptionSecretCode(PUSH_SUBSCRIPTION_SECRET_CODES.morningSubscribe + '呀'), null); +});