Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ba95c6229 | |||
| c58a23d155 | |||
| c7832e1869 |
@@ -94,7 +94,10 @@ export function looksLikeScheduledTaskNonDelivery(text, { readyPaths = [] } = {}
|
||||
if (/public\/[^\s]+\.html/i.test(normalized)) return false;
|
||||
if (/页面链接/u.test(normalized)) return false;
|
||||
if (/(?:已生成|已完成|交付).{0,24}(?:页面|链接|结果)/u.test(normalized)) return false;
|
||||
return normalized.length < 40;
|
||||
if (/^验证成功[。!!]?$/u.test(normalized)) return false;
|
||||
if (/^(?:任务)?(?:已)?完成[。!!]?$/u.test(normalized)) return false;
|
||||
if (/^【验证】/u.test(normalized) && normalized.length >= 6) return false;
|
||||
return normalized.length < 12;
|
||||
}
|
||||
|
||||
export async function finalizeScheduledTaskPageDelivery({
|
||||
|
||||
@@ -36,6 +36,9 @@ test('looksLikeScheduledTaskNonDelivery detects clarification replies', () => {
|
||||
looksLikeScheduledTaskNonDelivery('好的', { readyPaths: ['public/news.html'] }),
|
||||
false,
|
||||
);
|
||||
assert.equal(looksLikeScheduledTaskNonDelivery('验证成功'), false);
|
||||
assert.equal(looksLikeScheduledTaskNonDelivery('已完成'), false);
|
||||
assert.equal(looksLikeScheduledTaskNonDelivery('好的'), true);
|
||||
});
|
||||
|
||||
test('extractScheduledTaskDeliveryText reads last assistant message', () => {
|
||||
|
||||
@@ -25,6 +25,23 @@ const WEEKDAY_LABELS = [
|
||||
['周六', '星期六'],
|
||||
];
|
||||
|
||||
const META_TASK_SPEC_PATTERNS = [
|
||||
/^我要?(?:做|创建|设置|弄)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u,
|
||||
/^帮我?(?:做|创建|设置|弄)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u,
|
||||
/^请?(?:帮我)?(?:做|创建|设置|弄)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u,
|
||||
/^定时(?:自动)?执行(?:任务)?$/u,
|
||||
/^执行(?:任务)?$/u,
|
||||
/^做(?:一个|个)?(?:定时(?:自动)?)?(?:执行)?任务$/u,
|
||||
/^创建(?:一个|个)?定时(?:自动)?执行(?:任务)?$/u,
|
||||
/^想(?:要)?(?:做|创建|设置)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u,
|
||||
];
|
||||
|
||||
function isMetaScheduledTaskSpec(spec) {
|
||||
const normalized = String(spec ?? '').replace(/\s+/g, '').trim();
|
||||
if (!normalized) return true;
|
||||
return META_TASK_SPEC_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
function wantsScheduledTaskAutomation(compact) {
|
||||
if (SCHEDULE_MARKERS.test(compact)) return true;
|
||||
if (!RECURRENCE_MARKERS.test(compact)) return false;
|
||||
@@ -48,6 +65,7 @@ function extractScheduledTaskSpec(text) {
|
||||
const original = String(text ?? '').trim();
|
||||
if (!original) return null;
|
||||
let spec = original
|
||||
.replace(/^(?:不是|别是|并非)(?:待办|代办|提醒|日程|闹钟)[,,、::\s]*/u, '')
|
||||
.replace(
|
||||
/^(?:帮我)?(?:设|设置|创建|添加|想创建)(?:一个|个)?(?:定时(?:自动)?任务|定时执行任务)?[::,,、\s]*/u,
|
||||
'',
|
||||
@@ -62,9 +80,12 @@ function extractScheduledTaskSpec(text) {
|
||||
)
|
||||
.replace(/(?:周[一二三四五六日天]|星期[一二三四五六日天])/gu, ' ')
|
||||
.replace(/(?:帮我|请|麻烦)/gu, ' ')
|
||||
.replace(/^执行[,,、::\s]+/u, '')
|
||||
.replace(/[,,、::\s]+执行$/u, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (!spec || spec.length < 3) return null;
|
||||
if (isMetaScheduledTaskSpec(spec)) return null;
|
||||
if (!EXECUTE_VERBS.test(spec.replace(/\s+/g, ''))) return null;
|
||||
return spec;
|
||||
}
|
||||
@@ -302,6 +323,7 @@ export function formatScheduledTaskListReply(tasks = []) {
|
||||
|
||||
export {
|
||||
extractScheduledTaskSpec,
|
||||
isMetaScheduledTaskSpec,
|
||||
parseWeekday,
|
||||
resolveOnceRunAtLocal,
|
||||
};
|
||||
|
||||
@@ -90,3 +90,24 @@ test('isScheduledTaskIntent excludes none', () => {
|
||||
assert.equal(isScheduledTaskIntent({ action: 'create_scheduled_task' }), true);
|
||||
assert.equal(isScheduledTaskIntent({ action: 'none' }), false);
|
||||
});
|
||||
|
||||
test('meta scheduled-task phrases require task_spec clarification', () => {
|
||||
for (const text of [
|
||||
'我要做一个定时执行任务',
|
||||
'定时自动执行任务',
|
||||
'不是待办,定时执行任务',
|
||||
]) {
|
||||
const intent = parseScheduledTaskIntent(text);
|
||||
assert.equal(intent.action, 'create_scheduled_task', text);
|
||||
assert.ok(intent.needsClarification?.includes('task_spec'), text);
|
||||
assert.ok(intent.needsClarification?.includes('schedule'), text);
|
||||
assert.equal(extractScheduledTaskSpec(text), null, text);
|
||||
}
|
||||
});
|
||||
|
||||
test('combined phrase keeps substantive taskSpec after time prefix', () => {
|
||||
const text = '帮我创建一个定时执行任务,23:00执行,搜索今日新闻';
|
||||
const intent = parseScheduledTaskIntent(text);
|
||||
assert.equal(intent.needsClarification, undefined);
|
||||
assert.match(intent.taskSpec, /搜索今日新闻/u);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,16 @@ function parseArgs(argv) {
|
||||
|
||||
function formatRunAtLocal(epochMs, timezone = tz) {
|
||||
const parts = getLocalParts(epochMs, timezone);
|
||||
return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')} ${String(parts.hour).padStart(2, '0')}:${String(parts.minute).padStart(2, '0')}`;
|
||||
let minute = parts.minute;
|
||||
let hour = parts.hour;
|
||||
if (parts.second > 0) {
|
||||
minute += 1;
|
||||
if (minute >= 60) {
|
||||
minute = 0;
|
||||
hour += 1;
|
||||
}
|
||||
}
|
||||
return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function testIntentLayer() {
|
||||
@@ -195,7 +204,7 @@ async function testLivePortalWorker(pool, userId, { dueSeconds }) {
|
||||
const created = await scheduledTaskService.createTask({
|
||||
userId,
|
||||
title: `${TEST_TITLE_PREFIX}live_portal`,
|
||||
taskSpec: '用三句话总结今天的热点新闻(验证脚本,回复文本即可,不要生成页面)',
|
||||
taskSpec: '【验证】直接回复一行文字:验证成功。禁止追问、禁止生成页面、禁止调用任何工具。',
|
||||
recurrence: 'once',
|
||||
runAtLocal: formatRunAtLocal(runAtMs, tz),
|
||||
notifyChannel: 'web',
|
||||
@@ -203,10 +212,16 @@ async function testLivePortalWorker(pool, userId, { dueSeconds }) {
|
||||
});
|
||||
pass('Live 任务已创建', `${waitSeconds}s 后执行,id=${created.id}`);
|
||||
|
||||
console.log(`\n等待 Portal worker 执行(最多 ${waitSeconds + 120}s)…`);
|
||||
const executionTimeoutMs = Number(
|
||||
process.env.H5_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS
|
||||
?? process.env.VERIFY_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS
|
||||
?? 180_000,
|
||||
);
|
||||
const waitBudgetMs = waitSeconds * 1000 + executionTimeoutMs + 60_000;
|
||||
console.log(`\n等待 Portal worker 执行(最多 ${Math.ceil(waitBudgetMs / 1000)}s,含 Agent 超时 ${Math.ceil(executionTimeoutMs / 1000)}s)…`);
|
||||
console.log('请确认 Portal 进程已开启 scheduled task worker(H5_SCHEDULED_TASK_WORKER_ENABLED=1,或未设置时 H5_REMINDER_WORKER_ENABLED=1)\n');
|
||||
|
||||
const deadline = Date.now() + (waitSeconds + 120) * 1000;
|
||||
const deadline = Date.now() + waitBudgetMs;
|
||||
let terminal = null;
|
||||
while (Date.now() < deadline) {
|
||||
const [rows] = await pool.query(
|
||||
@@ -214,6 +229,11 @@ async function testLivePortalWorker(pool, userId, { dueSeconds }) {
|
||||
[created.id],
|
||||
);
|
||||
terminal = rows[0];
|
||||
if (!terminal) {
|
||||
fail('Live Portal worker', `任务 ${created.id} 在数据库中消失`);
|
||||
await cleanupTestRows(pool, userId);
|
||||
return;
|
||||
}
|
||||
if (terminal?.status === 'completed' || terminal?.status === 'failed') break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
@@ -222,6 +242,10 @@ async function testLivePortalWorker(pool, userId, { dueSeconds }) {
|
||||
pass('Live Portal worker', `任务 completed,updated_at=${terminal.updated_at}`);
|
||||
} else if (terminal?.status === 'failed') {
|
||||
fail('Live Portal worker', terminal.last_error ?? 'failed');
|
||||
} else if (terminal?.status === 'running') {
|
||||
fail('Live Portal worker', `Agent 仍在执行中(status=running);可提高 H5_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS 或检查 Goose 连通性`);
|
||||
} else if (terminal?.status === 'active' && terminal?.last_error) {
|
||||
fail('Live Portal worker', `已失败并重试调度:${terminal.last_error}`);
|
||||
} else {
|
||||
fail('Live Portal worker', `超时,最后 status=${terminal?.status ?? 'unknown'}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user