Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
const DEFAULT_TIMEZONE = 'Asia/Shanghai';
|
||||
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
export function normalizeTimezone(timezone) {
|
||||
return String(timezone || process.env.H5_DEFAULT_TIMEZONE || DEFAULT_TIMEZONE).trim() || DEFAULT_TIMEZONE;
|
||||
}
|
||||
|
||||
export function getLocalParts(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
|
||||
const formatter = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: normalizeTimezone(timezone),
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
const parts = Object.fromEntries(
|
||||
formatter.formatToParts(new Date(epochMs)).map((part) => [part.type, part.value]),
|
||||
);
|
||||
return {
|
||||
year: Number(parts.year),
|
||||
month: Number(parts.month),
|
||||
day: Number(parts.day),
|
||||
hour: Number(parts.hour === '24' ? 0 : parts.hour),
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
};
|
||||
}
|
||||
|
||||
export function localDateKey(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
|
||||
const parts = getLocalParts(epochMs, timezone);
|
||||
return `${parts.year}-${pad2(parts.month)}-${pad2(parts.day)}`;
|
||||
}
|
||||
|
||||
export function localDateLabel(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
|
||||
const parts = getLocalParts(epochMs, timezone);
|
||||
return `${parts.month}月${parts.day}日`;
|
||||
}
|
||||
|
||||
export function startOfLocalDay(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
|
||||
const parts = getLocalParts(epochMs, timezone);
|
||||
return zonedTimeToEpochMs(
|
||||
{
|
||||
year: parts.year,
|
||||
month: parts.month,
|
||||
day: parts.day,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
},
|
||||
timezone,
|
||||
);
|
||||
}
|
||||
|
||||
export function addLocalDays(epochMs, days, timezone = DEFAULT_TIMEZONE) {
|
||||
const parts = getLocalParts(epochMs, timezone);
|
||||
const utc = Date.UTC(parts.year, parts.month - 1, parts.day + Number(days || 0), parts.hour, parts.minute, parts.second);
|
||||
const shifted = new Date(utc);
|
||||
return zonedTimeToEpochMs(
|
||||
{
|
||||
year: shifted.getUTCFullYear(),
|
||||
month: shifted.getUTCMonth() + 1,
|
||||
day: shifted.getUTCDate(),
|
||||
hour: shifted.getUTCHours(),
|
||||
minute: shifted.getUTCMinutes(),
|
||||
second: shifted.getUTCSeconds(),
|
||||
},
|
||||
timezone,
|
||||
);
|
||||
}
|
||||
|
||||
export function zonedTimeToEpochMs(parts, timezone = DEFAULT_TIMEZONE) {
|
||||
const tz = normalizeTimezone(timezone);
|
||||
let guess = Date.UTC(
|
||||
Number(parts.year),
|
||||
Number(parts.month) - 1,
|
||||
Number(parts.day),
|
||||
Number(parts.hour ?? 0),
|
||||
Number(parts.minute ?? 0),
|
||||
Number(parts.second ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const actual = getLocalParts(guess, tz);
|
||||
const actualAsUtc = Date.UTC(
|
||||
actual.year,
|
||||
actual.month - 1,
|
||||
actual.day,
|
||||
actual.hour,
|
||||
actual.minute,
|
||||
actual.second,
|
||||
0,
|
||||
);
|
||||
const desiredAsUtc = Date.UTC(
|
||||
Number(parts.year),
|
||||
Number(parts.month) - 1,
|
||||
Number(parts.day),
|
||||
Number(parts.hour ?? 0),
|
||||
Number(parts.minute ?? 0),
|
||||
Number(parts.second ?? 0),
|
||||
0,
|
||||
);
|
||||
const delta = actualAsUtc - desiredAsUtc;
|
||||
if (delta === 0) break;
|
||||
guess -= delta;
|
||||
}
|
||||
return guess;
|
||||
}
|
||||
|
||||
export function nextDailyRunAt({ hour, minute = 0, timezone = DEFAULT_TIMEZONE, now = Date.now() }) {
|
||||
const tz = normalizeTimezone(timezone);
|
||||
const todayStart = startOfLocalDay(now, tz);
|
||||
const todayParts = getLocalParts(todayStart, tz);
|
||||
let runAt = zonedTimeToEpochMs(
|
||||
{
|
||||
year: todayParts.year,
|
||||
month: todayParts.month,
|
||||
day: todayParts.day,
|
||||
hour: Number(hour),
|
||||
minute: Number(minute),
|
||||
second: 0,
|
||||
},
|
||||
tz,
|
||||
);
|
||||
if (runAt <= now) {
|
||||
const tomorrowStart = addLocalDays(todayStart, 1, tz);
|
||||
const tomorrowParts = getLocalParts(tomorrowStart, tz);
|
||||
runAt = zonedTimeToEpochMs(
|
||||
{
|
||||
year: tomorrowParts.year,
|
||||
month: tomorrowParts.month,
|
||||
day: tomorrowParts.day,
|
||||
hour: Number(hour),
|
||||
minute: Number(minute),
|
||||
second: 0,
|
||||
},
|
||||
tz,
|
||||
);
|
||||
}
|
||||
return runAt;
|
||||
}
|
||||
|
||||
export function formatLocalTime(epochMs, timezone = DEFAULT_TIMEZONE) {
|
||||
const parts = getLocalParts(epochMs, timezone);
|
||||
return `${pad2(parts.hour)}:${pad2(parts.minute)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user