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:
+148
-4
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user