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>
This commit is contained in:
john
2026-09-23 10:07:32 +08:00
parent c36558e0f3
commit 028bb18dc0
7 changed files with 1029 additions and 4 deletions
+148 -4
View File
@@ -322,6 +322,131 @@ export async function finalizeScheduledTaskPageDelivery({
return readyPaths;
}
function scheduledTaskSleep(delayMs) {
return new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
}
/**
* 等 Agent 把预期 HTML 写入磁盘并稳定(大小连续两次不变)。
* 用于在 Goose 迟迟不发 Finish 时提前结束生成,避免空等会话收尾。
*/
export async function waitForExpectedPageFile({
publishDir,
relativePath,
timeoutMs = 15 * 60 * 1000,
pollIntervalMs = 2000,
stablePolls = 2,
minBytes = 100,
isPageReady = null,
sleepFn = scheduledTaskSleep,
} = {}) {
const normalized = String(relativePath ?? '').trim().replace(/^\/+/, '');
if (!normalized || !publishDir) return null;
const filePath = path.join(publishDir, normalized);
const deadline = Date.now() + Math.max(1, Number(timeoutMs) || 1);
let lastSize = -1;
let stable = 0;
while (Date.now() < deadline) {
try {
if (fs.existsSync(filePath)) {
const size = fs.statSync(filePath).size;
if (size >= minBytes && size === lastSize) {
stable += 1;
if (stable >= Math.max(1, Number(stablePolls) || 1)) {
if (typeof isPageReady === 'function') {
const html = fs.readFileSync(filePath, 'utf8');
if (!isPageReady(html)) {
stable = 0;
await sleepFn(Math.max(250, Number(pollIntervalMs) || 2000));
continue;
}
}
return normalized;
}
} else {
stable = 0;
lastSize = size;
}
} else {
stable = 0;
lastSize = -1;
}
} catch {
stable = 0;
lastSize = -1;
}
await sleepFn(Math.max(250, Number(pollIntervalMs) || 2000));
}
return null;
}
async function awaitGooseSessionOrExpectedPage({
task,
publishDir,
tkmindProxy,
sessionId,
requestId,
userMessage,
timeoutMs,
logger,
}) {
const sessionWait = tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
task.userId,
sessionId,
requestId,
userMessage,
{ timeoutMs },
);
const expectedPath = String(task.expectedRelativePath ?? '').trim();
if (!expectedPath || !publishDir) {
await sessionWait;
return { earlyReadyPath: null };
}
let earlyReadyPath = null;
const pageWait = waitForExpectedPageFile({
publishDir,
relativePath: expectedPath,
timeoutMs,
isPageReady: typeof task.isExpectedPageReady === 'function'
? task.isExpectedPageReady
: null,
}).then((found) => (found
? 'page'
: new Promise(() => {})));
try {
const winner = await Promise.race([
sessionWait.then(() => 'session'),
pageWait,
]);
if (winner === 'page') {
earlyReadyPath = expectedPath;
sessionWait.catch((error) => {
logger.warn?.('[ScheduledTask] session still open after page materialized; continuing', {
taskId: task.id,
sessionId,
message: error instanceof Error ? error.message : String(error),
});
});
logger.info?.('[ScheduledTask] proceeding without session Finish; page materialized', {
taskId: task.id,
sessionId,
relativePath: expectedPath,
});
}
} catch (error) {
pageWait.catch(() => {});
sessionWait.catch(() => {});
throw error;
}
return { earlyReadyPath };
}
export async function awaitScheduledTaskPageDelivery({
pool,
userId,
@@ -689,13 +814,16 @@ export async function executeScheduledTask(task, {
requestId,
});
await tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
task.userId,
const { earlyReadyPath } = await awaitGooseSessionOrExpectedPage({
task,
publishDir,
tkmindProxy,
sessionId,
requestId,
userMessage,
{ timeoutMs },
);
timeoutMs,
logger,
});
messages = await refreshScheduledTaskMessages({
userId: task.userId,
@@ -703,6 +831,22 @@ export async function executeScheduledTask(task, {
tkmindProxy,
sessionSnapshotService,
});
if (earlyReadyPath) {
let deliveryText = extractScheduledTaskDeliveryText(messages, task);
if (task.userId) {
const links = [`${resolveScheduledTaskPublicBaseUrl()}/MindSpace/${task.userId}/${earlyReadyPath}`];
deliveryText = `${deliveryText}\n\n页面链接:\n${links.join('\n')}`.trim();
}
return {
sessionId,
requestId,
deliveryText,
messages,
readyPaths: [earlyReadyPath],
executor: 'goose',
};
}
}
let deliveryText = extractScheduledTaskDeliveryText(messages, task);