#!/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);