diff --git a/wechat-news-morning-draft.mjs b/wechat-news-morning-draft.mjs index afbdc35..d37908b 100644 --- a/wechat-news-morning-draft.mjs +++ b/wechat-news-morning-draft.mjs @@ -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); diff --git a/wechat-news-morning-draft.test.mjs b/wechat-news-morning-draft.test.mjs index 063ea40..b89fded 100644 --- a/wechat-news-morning-draft.test.mjs +++ b/wechat-news-morning-draft.test.mjs @@ -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);