feat(wechat): add secret codes for morning subscribe and news push
Memind CI / Test, build, and release guards (push) Successful in 4m14s
Memind CI / Test, build, and release guards (push) Successful in 4m14s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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`,该分支保留仅用于只读追溯,不是待合并开发分支。**
|
||||
|
||||
@@ -3967,6 +3967,9 @@ export function createWechatMpService({
|
||||
sourceMessageId: inbound.msgId ?? null,
|
||||
wechatSubscribeMorningLlmConfigService,
|
||||
llmProviderService,
|
||||
mysqlPool,
|
||||
h5Root,
|
||||
env,
|
||||
logger,
|
||||
}).catch((err) => {
|
||||
logger.warn?.(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user