feat(wechat): expose today news morning status and strict preview APIs
Memind CI / Test, build, and release guards (push) Failing after 6m14s
Memind CI / Test, build, and release guards (push) Failing after 6m14s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+115
-14
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user