028bb18dc0
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>
129 lines
4.1 KiB
JavaScript
129 lines
4.1 KiB
JavaScript
#!/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);
|