feat(wechat): add secret codes for morning subscribe and news push
Memind CI / Test, build, and release guards (push) Successful in 4m14s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-20 19:10:27 +08:00
parent 830f8a4011
commit bb6aa75b3b
6 changed files with 148 additions and 0 deletions
+24
View File
@@ -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`,该分支保留仅用于只读追溯,不是待合并开发分支。**
+3
View File
@@ -3967,6 +3967,9 @@ export function createWechatMpService({
sourceMessageId: inbound.msgId ?? null,
wechatSubscribeMorningLlmConfigService,
llmProviderService,
mysqlPool,
h5Root,
env,
logger,
}).catch((err) => {
logger.warn?.(
+34
View File
@@ -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 '绑定账号后可查看订阅状态。';
@@ -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);
});
+26
View File
@@ -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;
}
@@ -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);
});