feat(wechat): expose today news morning status and strict preview APIs
Memind CI / Test, build, and release guards (push) Failing after 6m14s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-11 08:52:58 +08:00
parent 62caf4134b
commit 1f6e8c9bad
2 changed files with 141 additions and 14 deletions
+115 -14
View File
@@ -680,7 +680,26 @@ export function createWechatNewsMorningDraftService(
return accessTokenCache.token;
}
async function resolvePreview(configOverride = null) {
function buildPageSummary(config, page, { now = Date.now() } = {}) {
const publicBaseUrl = resolveDailyNewsPublicBaseUrl(env);
const publicUrl = buildPublicUrl(publicBaseUrl, config.sourceUserId, page.relativePath);
const dateParts = formatLocalDateParts(new Date(now), config.timezone);
return {
slug: page.slug,
relativePath: page.relativePath,
publicUrl,
modifiedAt: page.modifiedAt,
hasThumb: Boolean(page.thumbPath),
isTodayPage: isNewsMorningPageForToday(page, {
date: new Date(now),
timezone: config.timezone,
}),
dateKey: dateParts.iso,
expectedSlug: `daily-news-${dateParts.mmdd}`,
};
}
async function resolvePreview(configOverride = null, { requireToday = false, now = Date.now() } = {}) {
const config = configOverride ?? mergeConfig(await readConfigRow());
if (!config.sourceUserId) {
throw new Error('请先配置新闻早报来源用户 ID');
@@ -689,25 +708,23 @@ export function createWechatNewsMorningDraftService(
h5Root,
userId: config.sourceUserId,
slugPattern: config.pageSlugPattern,
date: new Date(now),
timezone: config.timezone,
});
if (!page) {
throw new Error(`未找到匹配 ${config.pageSlugPattern} 的新闻早报页面`);
}
const pageSummary = buildPageSummary(config, page, { now });
if (requireToday && !pageSummary.isTodayPage) {
throw new Error(`今日(${pageSummary.dateKey})新闻早报尚未生成,当前最新页面为 ${page.slug}`);
}
const html = fs.readFileSync(page.localPath, 'utf8');
const publicBaseUrl = resolveDailyNewsPublicBaseUrl(env);
const publicUrl = buildPublicUrl(publicBaseUrl, config.sourceUserId, page.relativePath);
const article = isDailyNewsFormat(html)
? buildDailyNewsWechatDraftArticle({ html, publicUrl })
: convertNewsPageHtmlToWechatArticle(html, { publicUrl });
? buildDailyNewsWechatDraftArticle({ html, publicUrl: pageSummary.publicUrl })
: convertNewsPageHtmlToWechatArticle(html, { publicUrl: pageSummary.publicUrl });
return {
config,
page: {
slug: page.slug,
relativePath: page.relativePath,
publicUrl,
modifiedAt: page.modifiedAt,
hasThumb: Boolean(page.thumbPath),
},
page: pageSummary,
article,
template: {
version: NEWS_MORNING_TEMPLATE_VERSION,
@@ -716,6 +733,25 @@ export function createWechatNewsMorningDraftService(
};
}
async function listRunsSince(sinceMs, { limit = 20 } = {}) {
await ensureReady();
const safeLimit = Math.min(Math.max(Number(limit) || 20, 1), 100);
const [rows] = await pool.query(
`SELECT id, status, page_slug AS pageSlug, page_url AS pageUrl,
draft_media_id AS draftMediaId, error_message AS errorMessage,
triggered_by AS triggeredBy, created_at AS createdAt
FROM ${RUNS_TABLE}
WHERE created_at >= ?
ORDER BY created_at DESC
LIMIT ?`,
[sinceMs, safeLimit],
);
return rows.map((row) => ({
...row,
createdAt: Number(row.createdAt ?? 0),
}));
}
async function recordRun({
status,
pageSlug = null,
@@ -809,8 +845,72 @@ export function createWechatNewsMorningDraftService(
return this.getConfig();
},
async preview() {
return resolvePreview();
async preview(options = {}) {
return resolvePreview(null, options);
},
async previewToday() {
return resolvePreview(null, { requireToday: true });
},
async getTodayStatus({ now = Date.now() } = {}) {
const config = await this.getConfig();
const dateParts = formatLocalDateParts(new Date(now), config.timezone);
const expectedSlug = `daily-news-${dateParts.mmdd}`;
if (!config.sourceUserId) {
return {
configured: false,
dateKey: dateParts.iso,
expectedSlug,
todayPageReady: false,
page: null,
autoGenerateEnabled: config.autoGenerateEnabled,
autoPushEnabled: config.autoPushEnabled,
pushHour: config.pushHour,
pushMinute: config.pushMinute,
generateLeadMinutes: config.generateLeadMinutes,
message: '请先配置新闻早报来源用户 ID',
runs: {
generate: null,
push: null,
},
};
}
const page = findLatestNewsMorningPage({
h5Root,
userId: config.sourceUserId,
slugPattern: config.pageSlugPattern,
date: new Date(now),
timezone: config.timezone,
});
const pageSummary = page ? buildPageSummary(config, page, { now }) : null;
const todayPageReady = Boolean(pageSummary?.isTodayPage);
const dayStart = startOfLocalDay(now, config.timezone);
const runs = await listRunsSince(dayStart, { limit: 20 });
const generate = runs.find((run) => run.triggeredBy === 'auto-generate') ?? null;
const push = runs.find((run) => run.triggeredBy === 'auto-push') ?? null;
return {
configured: true,
dateKey: dateParts.iso,
expectedSlug,
todayPageReady,
page: todayPageReady ? pageSummary : null,
latestPageSlug: pageSummary && !todayPageReady ? pageSummary.slug : null,
autoGenerateEnabled: config.autoGenerateEnabled,
autoPushEnabled: config.autoPushEnabled,
pushHour: config.pushHour,
pushMinute: config.pushMinute,
generateLeadMinutes: config.generateLeadMinutes,
message: todayPageReady
? null
: `今日(${dateParts.iso})新闻早报尚未生成`,
runs: {
generate,
push,
},
};
},
async pushDraft({ triggeredBy = null, dryRun = false } = {}) {
@@ -833,6 +933,7 @@ export function createWechatNewsMorningDraftService(
h5Root,
userId: config.sourceUserId,
slugPattern: config.pageSlugPattern,
timezone: config.timezone,
});
if (page?.thumbPath) {
thumbBuffer = fs.readFileSync(page.thumbPath);
+26
View File
@@ -168,6 +168,32 @@ test('news morning draft service persists config and records failed push', async
assert.equal(runs[0].status, 'failed');
});
test('getTodayStatus and previewToday require today dated page', async () => {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'news-draft-today-'));
const userId = 'user-today';
const publicDir = path.join(root, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
await fs.promises.mkdir(publicDir, { recursive: true });
await fs.promises.writeFile(path.join(publicDir, 'daily-news-0910.html'), SAMPLE_HTML);
await fs.promises.writeFile(path.join(publicDir, 'daily-news-0911.html'), SAMPLE_HTML);
const service = createWechatNewsMorningDraftService(createPool(), {
mpConfig: { enabled: false },
h5Root: root,
env: {},
});
await service.updateConfig({ sourceUserId: userId });
const now = Date.UTC(2026, 8, 10, 22, 0, 0);
const status = await service.getTodayStatus({ now });
assert.equal(status.todayPageReady, true);
assert.equal(status.page?.slug, 'daily-news-0911');
assert.equal(status.expectedSlug, 'daily-news-0911');
const preview = await service.previewToday();
assert.equal(preview.page.isTodayPage, true);
assert.equal(preview.page.slug, 'daily-news-0911');
});
test('internals normalize booleans consistently', () => {
assert.equal(wechatNewsMorningDraftInternals.normalizeBoolean('1', false), true);
assert.equal(wechatNewsMorningDraftInternals.normalizeBoolean('off', true), false);