Files
memind/scripts/news-item-generate-page.mjs
T
john 028bb18dc0 feat(news): add single news-item page generation and early page-ready wait
Scheduled tasks can finish once the expected HTML is on disk and stable,
instead of waiting for Goose Finish. Adds news-item templates and scripts
for generating a single-event page and committing it as a WeChat draft.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 10:07:32 +08:00

129 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 阶段一:Agent 深度分析单条新闻并生成落地页。只生成,不推送。
*
* stdin { userId, newsItem, runId? }
* stdout { ok, pageSlug, pageUrl, relativePath, publishDir, html, check, executor }
*
* 落地页生成后即可点链接查看;后续的预览、编辑、推送都不再需要跑 Agent。
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { executeScheduledTask } from '../scheduled-task-executor.mjs';
import {
buildPublicUrl,
PUBLISH_ROOT_DIR,
resolvePublicBaseUrl,
} from '../user-publish.mjs';
import {
buildNewsItemGenerationTaskSpec,
validateNewsItemHtml,
} from '../news-item-templates.mjs';
import {
bootstrapAgentContext,
loadPushEnvironment,
readStdinPayload,
runScript,
} from './news-item-wechat-context.mjs';
function slugify(value) {
return String(value ?? '')
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 48) || 'news-item';
}
function buildGenerationTask(userId, newsItem, env, now = Date.now()) {
const date = new Date(now);
const mmdd = `${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`;
const slug = `news-item-${slugify(newsItem.canonicalTitle || newsItem.title)}-${mmdd}`;
const { style, taskSpec } = buildNewsItemGenerationTaskSpec(newsItem, env);
return {
id: `news-item-${slug}`,
userId,
title: `新闻深度稿:${newsItem.canonicalTitle || newsItem.title}`,
taskSpec: [
taskSpec,
'',
`输出文件名必须是 public/${slug}.html(可覆盖同名旧文件)。`,
`data-style 必须设为 ${style}。`,
].join('\n'),
recurrence: 'once',
timezone: 'Asia/Shanghai',
notifyChannel: 'web',
expectedSlug: slug,
expectedRelativePath: `public/${slug}.html`,
layoutStyle: style,
isExpectedPageReady: (html) => validateNewsItemHtml(html, { style }).ok,
};
}
function resolveTimeoutMs(env) {
return Math.max(
5 * 60_000,
Number(env.MEMIND_NEWS_ITEM_GENERATE_TIMEOUT_MS ?? 20 * 60_000) || 20 * 60_000,
);
}
async function main() {
const env = loadPushEnvironment(process.env);
const payload = readStdinPayload();
const userId = String(payload.userId ?? '').trim();
const newsItem = payload.newsItem ?? {};
if (!userId) throw new Error('缺少 userId');
const services = await bootstrapAgentContext(env);
try {
const task = buildGenerationTask(userId, newsItem, env);
const execution = await executeScheduledTask(task, {
userAuth: services.userAuth,
tkmindProxy: services.tkmindProxy,
agentRunGateway: services.agentRunGateway,
cursorExecutorPolicyService: services.cursorExecutorPolicyService,
sessionSnapshotService: services.sessionSnapshotService,
pool: services.pool,
h5Root: services.h5Root,
timeoutMs: resolveTimeoutMs(env),
logger: console,
});
const publishDir = path.join(services.h5Root, PUBLISH_ROOT_DIR, userId);
const relativePath = execution.readyPaths?.[0]
?? (fs.existsSync(path.join(publishDir, task.expectedRelativePath))
? task.expectedRelativePath
: null);
if (!relativePath) {
throw new Error('Agent 未生成 public/*.html 页面');
}
const localPath = path.join(publishDir, relativePath);
if (!fs.existsSync(localPath)) throw new Error(`生成页面不存在:${relativePath}`);
const html = fs.readFileSync(localPath, 'utf8');
const check = validateNewsItemHtml(html, { style: task.layoutStyle });
if (!check.ok) {
throw Object.assign(
new Error(`生成页面不达标:${check.issues.join(';')}`),
{ code: 'news_item_page_substandard' },
);
}
return {
pageSlug: path.basename(relativePath, '.html'),
pageUrl: buildPublicUrl(resolvePublicBaseUrl(env), userId, relativePath),
relativePath,
publishDir,
html,
check,
layoutStyle: task.layoutStyle,
executor: execution.executor ?? null,
};
} finally {
await services.close();
}
}
runScript(main);