Files
memind/scripts/run-tang-subscribe-morning-e2e-103.mjs
T
john d07815d317
Memind CI / Test, build, and release guards (push) Successful in 6m17s
feat(wechat): update subscribe welcome plaza picks to curated pages
Replace category links with Xinjiang travel, Plaza late-night food, and creative landing pages; sync missing E2E deps on 103 runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:03:27 +08:00

287 lines
9.5 KiB
JavaScript

#!/usr/bin/env node
/**
* 103 唐用户:推送新关注欢迎语 → 等待回复 1 → 验证/补跑早安提醒设置。
*
* Usage:
* node scripts/run-tang-subscribe-morning-e2e-103.mjs
* node scripts/run-tang-subscribe-morning-e2e-103.mjs --wait-seconds 600
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const HOST = 'john@58.38.22.103';
const REMOTE_ROOT = '/Users/john/Project/Memind';
const NODE103 = '/opt/homebrew/opt/node@24/bin/node';
const TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db';
function parseWaitSeconds(argv) {
for (let i = 0; i < argv.length; i += 1) {
if (argv[i] === '--wait-seconds') {
return Math.max(30, Number(argv[i + 1] ?? 600) || 600);
}
}
return 600;
}
function sh(cmd) {
execSync(cmd, { stdio: 'inherit' });
}
function scp(localRel, remoteAbs) {
sh(`scp -q ${path.join(root, localRel)} ${HOST}:${remoteAbs}`);
}
const waitSeconds = parseWaitSeconds(process.argv.slice(2));
const remoteRunner = `${REMOTE_ROOT}/.tmp-run-tang-subscribe-morning-e2e.mjs`;
const remoteScript = `
import path from 'node:path';
import mysql from 'mysql2/promise';
import { buildSubscribeWelcomeText } from './wechat/handlers/sync-replies.mjs';
import {
createSubscribeMorningReminderPendingStore,
handleSubscribeMorningReminderTurn,
isSubscribeMorningConfirmReply,
subscribeMorningReminderSchedule,
} from './wechat/subscribe-morning-reminder.mjs';
import { createScheduleService } from './schedule-service.mjs';
const TANG_USER_ID = ${JSON.stringify(TANG_USER_ID)};
const WAIT_SECONDS = ${waitSeconds};
const POLL_MS = 5000;
process.loadEnvFile(path.join('${REMOTE_ROOT}', '.env'));
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
const appId = process.env.H5_WECHAT_MP_APP_ID ?? process.env.WECHAT_MP_APP_ID;
const appSecret = process.env.H5_WECHAT_MP_APP_SECRET ?? process.env.WECHAT_MP_APP_SECRET;
const publicBaseUrl = (process.env.H5_PUBLIC_BASE_URL ?? 'https://m.tkmind.cn').replace(/\\/$/, '');
const bindPath = process.env.H5_WECHAT_MP_BIND_PATH?.trim() || '/auth/wechat/authorize?intent=login';
const timezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
const { hour, minute } = subscribeMorningReminderSchedule({ env: process.env });
if (!appId || !appSecret) throw new Error('missing wechat credentials');
async function fetchAccessToken() {
const tokenRes = await fetch('https://api.weixin.qq.com/cgi-bin/stable_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'client_credential', appid: appId, secret: appSecret }),
});
const tokenPayload = await tokenRes.json();
if (!tokenPayload.access_token) throw new Error(JSON.stringify(tokenPayload));
return tokenPayload.access_token;
}
async function sendWechatText(openid, text) {
const accessToken = await fetchAccessToken();
const sendRes = await fetch(
'https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=' + encodeURIComponent(accessToken),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ touser: openid, msgtype: 'text', text: { content: text } }),
},
);
const sendPayload = await sendRes.json();
if (Number(sendPayload.errcode ?? 0) !== 0) throw new Error(JSON.stringify(sendPayload));
return sendPayload;
}
async function findMorningScheduleItem(userId) {
const [rows] = await pool.query(
\`SELECT id, title, metadata_json, status, created_at
FROM h5_schedule_items
WHERE user_id = ? AND deleted_at IS NULL
AND JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.source')) = 'subscribe_morning_reminder'
ORDER BY created_at DESC
LIMIT 1\`,
[userId],
);
return rows?.[0] ?? null;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const [users] = await pool.query(
'SELECT id, username, display_name FROM h5_users WHERE id = ? LIMIT 1',
[TANG_USER_ID],
);
const user = users?.[0];
if (!user) throw new Error('tang user not found');
const [ident] = await pool.query(
'SELECT openid FROM h5_user_wechat_identities WHERE user_id = ? AND app_id = ? LIMIT 1',
[TANG_USER_ID, appId],
);
const openid = ident?.[0]?.openid;
if (!openid) throw new Error('tang not bound to wechat');
const pendingStore = createSubscribeMorningReminderPendingStore({ mysqlPool: pool });
const scheduleService = createScheduleService(pool, { defaultTimezone: timezone });
const welcomeText = buildSubscribeWelcomeText({
bindUrl: publicBaseUrl + bindPath,
introUrl: process.env.H5_WECHAT_MP_SUBSCRIBE_INTRO_URL?.trim() || undefined,
plazaHomeUrl: process.env.H5_WECHAT_MP_SUBSCRIBE_PLAZA_HOME_URL?.trim()
|| (process.env.PLAZA_PUBLIC_BASE?.trim()
? process.env.PLAZA_PUBLIC_BASE.trim().replace(/\\/$/, '') + '/plaza'
: undefined),
morningReminderEnabled: true,
morningReminderHour: hour,
morningReminderMinute: minute,
});
await pendingStore.setPending({ appId, openid });
await sendWechatText(openid, welcomeText);
const baselineAt = Date.now();
console.log(JSON.stringify({
phase: 'sent',
userId: TANG_USER_ID,
username: user.username,
displayName: user.display_name,
openid: openid.slice(0, 10) + '...',
baselineAt,
waitSeconds: WAIT_SECONDS,
instruction: '请唐在微信里回复 1 确认每日早安提醒',
welcomePreview: welcomeText.slice(0, 240) + (welcomeText.length > 240 ? '...' : ''),
}, null, 2));
const seenMsgIds = new Set();
const deadline = Date.now() + WAIT_SECONDS * 1000;
let outcome = null;
while (Date.now() < deadline) {
const [rows] = await pool.query(
\`SELECT msg_id, agent_text, display_text, created_at
FROM h5_wechat_mp_message_details
WHERE app_id = ? AND openid = ? AND msg_type = 'text' AND created_at >= ?
ORDER BY created_at ASC\`,
[appId, openid, baselineAt - 5000],
);
for (const row of rows) {
const msgId = String(row.msg_id ?? '');
if (!msgId || seenMsgIds.has(msgId)) continue;
seenMsgIds.add(msgId);
const text = String(row.agent_text ?? row.display_text ?? '').trim();
console.log(JSON.stringify({
phase: 'inbound',
msgId,
text,
createdAt: Number(row.created_at),
}));
if (!isSubscribeMorningConfirmReply(text)) {
console.log(JSON.stringify({ phase: 'ignored', reason: 'not_confirm_1', msgId, text }));
continue;
}
console.log(JSON.stringify({ phase: 'confirm_detected', msgId, text }));
await sleep(15000);
let item = await findMorningScheduleItem(TANG_USER_ID);
let handlerReply = null;
let handlerSource = item ? 'production_or_existing' : 'e2e_fallback';
if (!item) {
handlerReply = await handleSubscribeMorningReminderTurn({
appId,
openid,
text,
pendingStore,
scheduleService,
boundUser: { userId: TANG_USER_ID },
timezone,
hour,
minute,
bindUrl: publicBaseUrl + bindPath,
sourceMessageId: msgId,
});
if (handlerReply) {
await sendWechatText(openid, handlerReply);
}
item = await findMorningScheduleItem(TANG_USER_ID);
}
const [reminders] = item
? await pool.query(
\`SELECT id, remind_at, channel, status
FROM h5_schedule_reminders
WHERE user_id = ? AND item_id = ?
ORDER BY created_at DESC LIMIT 1\`,
[TANG_USER_ID, item.id],
)
: [[]];
outcome = {
ok: Boolean(item),
handlerSource,
handlerReply,
scheduleItem: item
? {
id: item.id,
title: item.title,
status: item.status,
metadata: item.metadata_json,
}
: null,
reminder: reminders?.[0] ?? null,
msgId,
replyText: text,
};
break;
}
if (outcome) break;
await sleep(POLL_MS);
}
if (!outcome) {
console.log(JSON.stringify({
ok: false,
phase: 'timeout',
waitedSeconds: WAIT_SECONDS,
message: '等待时间内未收到唐回复 1',
}, null, 2));
process.exitCode = 2;
} else {
console.log(JSON.stringify({ phase: 'result', ...outcome }, null, 2));
if (!outcome.ok) process.exitCode = 1;
}
await pool.end();
`.trim();
const filesToSync = [
['schedule-time.mjs', `${REMOTE_ROOT}/schedule-time.mjs`],
['schedule-intent.mjs', `${REMOTE_ROOT}/schedule-intent.mjs`],
['schedule-utterance-normalize.mjs', `${REMOTE_ROOT}/schedule-utterance-normalize.mjs`],
['schedule-service.mjs', `${REMOTE_ROOT}/schedule-service.mjs`],
['wechat-subscribe-morning-llm.mjs', `${REMOTE_ROOT}/wechat-subscribe-morning-llm.mjs`],
['wechat/morning-greeting-library.mjs', `${REMOTE_ROOT}/wechat/morning-greeting-library.mjs`],
['wechat/subscribe-morning-reminder.mjs', `${REMOTE_ROOT}/wechat/subscribe-morning-reminder.mjs`],
['wechat/handlers/sync-replies.mjs', `${REMOTE_ROOT}/wechat/handlers/sync-replies.mjs`],
['wechat/user/display-name.mjs', `${REMOTE_ROOT}/wechat/user/display-name.mjs`],
];
for (const [rel, remoteAbs] of filesToSync) {
sh(`ssh -o BatchMode=yes ${HOST} 'mkdir -p $(dirname ${remoteAbs})'`);
scp(rel, remoteAbs);
}
const localRunner = path.join(root, '.tmp-run-tang-subscribe-morning-e2e.mjs');
fs.writeFileSync(localRunner, remoteScript);
scp('.tmp-run-tang-subscribe-morning-e2e.mjs', remoteRunner);
try {
sh(`ssh -o BatchMode=yes ${HOST} 'cd ${REMOTE_ROOT} && ${NODE103} ${remoteRunner}; ec=$?; rm -f ${remoteRunner}; exit $ec'`);
} finally {
fs.unlinkSync(localRunner);
}