feat(wechat): add Intent Transaction Layer with unified task schema
Introduce Draft → Confirm → Commit flow for WeChat schedule intents behind feature flags, plus h5_tasks dual-write/read aggregation and rollout scripts so reminders and automations get explicit user confirmation before persisting. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Simulate ~1000 user utterances across reminder / schedule / automation tiers.
|
||||
* Reports routing accuracy, ambiguity, and agent fallthrough rate.
|
||||
*/
|
||||
import {
|
||||
parseScheduleIntent,
|
||||
shouldUseScheduleAssistant,
|
||||
} from '../schedule-intent.mjs';
|
||||
import {
|
||||
isScheduledTaskIntent,
|
||||
parseScheduledTaskIntent,
|
||||
shouldUseScheduledTaskAutomation,
|
||||
} from '../scheduled-task-intent.mjs';
|
||||
|
||||
const TZ = 'Asia/Shanghai';
|
||||
const NOW = Date.UTC(2026, 7, 24, 4, 0, 0); // 2026-08-24 12:00 CST
|
||||
|
||||
const TIME_VARIANTS = [
|
||||
'早上6点', '上午9点', '中午12点', '下午2点', '下午14:30', '14:30', '晚上8点', '今晚9点半',
|
||||
'明天早上7点', '明天下午3点', '后天上午10点', '今天15点', '明早6点半', '下午两点半',
|
||||
];
|
||||
const REMINDER_VERBS = ['提醒我', '设置提醒', '设个提醒', '闹钟', '叫我', '到点提醒'];
|
||||
const REMINDER_TOPICS = [
|
||||
'开会', '项目计划例会', '吃药', '接孩子', '交周报', '还信用卡', 'Standup',
|
||||
'给老板发邮件', '健身', '订外卖', '复盘', '周会', '1对1',
|
||||
];
|
||||
const TODO_VERBS = ['帮我记一下', '记个待办', '添加待办', '设置一个代办', '先记一下'];
|
||||
const TODO_TOPICS = ['跟进合同', '买牛奶', '回复张三', '整理发票', '还书', '修空调'];
|
||||
const AUTO_TOPICS = [
|
||||
'做今日新闻页面', '生成天气预报页面', '整理待办摘要页面', '推送销售日报',
|
||||
'汇总行业资讯', '更新数据看板页面', '生成诗词页面',
|
||||
];
|
||||
const AUTO_RECURRENCE = ['每天', '每日', '每周一', '每周五', '定时'];
|
||||
|
||||
function cartesian(parts) {
|
||||
return parts.reduce(
|
||||
(acc, group) => acc.flatMap((prefix) => group.map((item) => [...prefix, item])),
|
||||
[[]],
|
||||
);
|
||||
}
|
||||
|
||||
function buildCorpus() {
|
||||
const buckets = {
|
||||
reminder: [],
|
||||
schedule: [],
|
||||
automation: [],
|
||||
edge: [],
|
||||
};
|
||||
|
||||
for (const time of TIME_VARIANTS) {
|
||||
for (const verb of REMINDER_VERBS) {
|
||||
for (const topic of REMINDER_TOPICS.slice(0, 6)) {
|
||||
buckets.reminder.push({
|
||||
text: `${time}${verb}${topic}`,
|
||||
expectedTier: 'reminder',
|
||||
expectedPath: 'preflight_or_agent',
|
||||
});
|
||||
buckets.reminder.push({
|
||||
text: `帮我设置提醒,${time},${topic}`,
|
||||
expectedTier: 'reminder',
|
||||
expectedPath: 'preflight_or_agent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const verb of TODO_VERBS) {
|
||||
for (const topic of TODO_TOPICS) {
|
||||
buckets.schedule.push({
|
||||
text: `${verb} ${topic}`,
|
||||
expectedTier: 'schedule',
|
||||
expectedPath: 'preflight',
|
||||
});
|
||||
buckets.schedule.push({
|
||||
text: `${verb}「${topic}」`,
|
||||
expectedTier: 'schedule',
|
||||
expectedPath: 'preflight',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const time of ['早上7点', '每天8点', '每日6点半', '每天早上7点', '每天7点']) {
|
||||
buckets.schedule.push({
|
||||
text: `${time}把当天待办发给我`,
|
||||
expectedTier: 'schedule',
|
||||
expectedPath: 'preflight',
|
||||
});
|
||||
}
|
||||
|
||||
for (const threshold of [10, 20, 50, 100, 200]) {
|
||||
buckets.schedule.push({
|
||||
text: `余额低于${threshold}元提醒我`,
|
||||
expectedTier: 'schedule',
|
||||
expectedPath: 'preflight',
|
||||
});
|
||||
}
|
||||
|
||||
for (const rec of AUTO_RECURRENCE) {
|
||||
for (const time of ['5点', '6点', '7点半', '8:00', '18点', '8点30分']) {
|
||||
for (const topic of AUTO_TOPICS) {
|
||||
buckets.automation.push({
|
||||
text: `${rec}${time}帮我${topic}`,
|
||||
expectedTier: 'automation',
|
||||
expectedPath: 'preflight_or_agent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buckets.edge.push(
|
||||
{ text: '明天早上六点去跑步,五点半提醒我', expectedTier: 'reminder', expectedPath: 'agent', note: '双时间点' },
|
||||
{ text: '每天6点帮我做今日新闻页面', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '每天6点提醒我开会', expectedTier: 'reminder', expectedPath: 'agent', note: '循环提醒' },
|
||||
{ text: '帮我生成一个唐诗页面', expectedTier: 'none', expectedPath: 'general' },
|
||||
{ text: '取消我的定时任务', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '看看我的待办', expectedTier: 'schedule', expectedPath: 'preflight' },
|
||||
{ text: '设置提醒', expectedTier: 'reminder', expectedPath: 'clarify', note: '缺时间' },
|
||||
{ text: '下午3点提醒我', expectedTier: 'reminder', expectedPath: 'clarify', note: '缺标题' },
|
||||
{ text: '不是待办,下午2点提醒我交报告', expectedTier: 'reminder', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '帮我设置一个代办明天早上六点去跑步记得在五点半的时候提醒我', expectedTier: 'reminder', expectedPath: 'agent' },
|
||||
{ text: '每周一7点整理待办摘要', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '今晚8点推送今日待办清单', expectedTier: 'schedule', expectedPath: 'agent_or_clarify', note: '待办清单' },
|
||||
{ text: '定时任务:每天8点生成天气页面', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '3小时后提醒我开会', expectedTier: 'reminder', expectedPath: 'agent', note: '相对时间' },
|
||||
{ text: '半小时后叫我', expectedTier: 'reminder', expectedPath: 'agent', note: '相对时间' },
|
||||
{ text: '周五下午3点项目评审提醒我', expectedTier: 'reminder', expectedPath: 'agent', note: '周几' },
|
||||
{ text: '8月30号下午2点提醒我续费', expectedTier: 'reminder', expectedPath: 'agent', note: '具体日期' },
|
||||
{ text: '提前15分钟提醒我开会', expectedTier: 'reminder', expectedPath: 'agent', note: '偏移提醒' },
|
||||
{ text: '会议是3点,提前10分钟提醒我', expectedTier: 'reminder', expectedPath: 'agent', note: '事件+偏移' },
|
||||
{ text: '查一下是否有执行的新闻任务', expectedTier: 'none', expectedPath: 'general', note: '误触' },
|
||||
{ text: '设个提醒下午3点开会', expectedTier: 'reminder', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '到点提醒下午2点交报告', expectedTier: 'reminder', expectedPath: 'preflight_or_agent' },
|
||||
{ text: '帮我设置提醒,下午 14:30 分开会,项目计划例会', expectedTier: 'reminder', expectedPath: 'preflight' },
|
||||
// --- Adversarial / ambiguous (expect clarify or correct layer, never wrong commit) ---
|
||||
{ text: '明天提醒我自动生成日报', expectedTier: 'ambiguous', expectedPath: 'clarify', note: 'adv:提醒+生成' },
|
||||
{ text: '帮我每天看看有没有新的招聘信息', expectedTier: 'automation', expectedPath: 'preflight_or_agent', note: 'adv:监控型自动化' },
|
||||
{ text: '设置一个任务,如果余额低于100提醒我', expectedTier: 'schedule', expectedPath: 'preflight_or_agent', note: 'adv:condition trigger' },
|
||||
{ text: '有没有我的新闻定时任务', expectedTier: 'none', expectedPath: 'general', note: 'adv:query guard' },
|
||||
{ text: '每天8点提醒我跑步', expectedTier: 'reminder', expectedPath: 'agent', note: 'adv:recurring reminder' },
|
||||
{ text: '每天8点帮我生成跑步报告', expectedTier: 'automation', expectedPath: 'preflight_or_agent', note: 'adv:notify vs act' },
|
||||
);
|
||||
|
||||
const noisePrefixes = ['那个', '嗯', '麻烦', '请', '能不能'];
|
||||
for (const prefix of noisePrefixes) {
|
||||
for (const base of buckets.reminder.slice(0, 30)) {
|
||||
buckets.edge.push({
|
||||
text: `${prefix}${base.text}`,
|
||||
expectedTier: 'reminder',
|
||||
expectedPath: 'preflight_or_agent',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sample(arr, n) {
|
||||
if (arr.length <= n) return arr;
|
||||
const step = arr.length / n;
|
||||
const out = [];
|
||||
for (let i = 0; i < n; i += 1) out.push(arr[Math.floor(i * step)]);
|
||||
return out;
|
||||
}
|
||||
|
||||
const target = {
|
||||
reminder: 400,
|
||||
schedule: 250,
|
||||
automation: 280,
|
||||
edge: 70,
|
||||
};
|
||||
|
||||
const cases = [];
|
||||
for (const [bucket, count] of Object.entries(target)) {
|
||||
for (const [index, item] of sample(buckets[bucket], count).entries()) {
|
||||
cases.push({ id: `${bucket}-${index}`, ...item });
|
||||
}
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
function classifyRoute(text) {
|
||||
const schedTask = parseScheduledTaskIntent(text, { now: NOW, timezone: TZ });
|
||||
const sched = parseScheduleIntent(text, { timezone: TZ, now: NOW });
|
||||
|
||||
if (isScheduledTaskIntent(schedTask)) {
|
||||
if (schedTask.action === 'create_scheduled_task') {
|
||||
return {
|
||||
tier: 'automation',
|
||||
path: schedTask.needsClarification?.length ? 'clarify' : 'preflight',
|
||||
action: schedTask.action,
|
||||
detail: schedTask,
|
||||
};
|
||||
}
|
||||
return {
|
||||
tier: 'automation',
|
||||
path: 'preflight',
|
||||
action: schedTask.action,
|
||||
detail: schedTask,
|
||||
};
|
||||
}
|
||||
|
||||
if (sched.action === 'create_timed_reminder') {
|
||||
return {
|
||||
tier: 'reminder',
|
||||
path: sched.needsClarification?.length ? 'clarify' : 'preflight',
|
||||
action: sched.action,
|
||||
detail: sched,
|
||||
};
|
||||
}
|
||||
|
||||
if (['create_todo', 'create_daily_todo_digest', 'create_balance_alert', 'query_schedule'].includes(sched.action)) {
|
||||
return {
|
||||
tier: 'schedule',
|
||||
path: sched.needsClarification?.length ? 'clarify' : 'preflight',
|
||||
action: sched.action,
|
||||
detail: sched,
|
||||
};
|
||||
}
|
||||
|
||||
if (sched.action === 'schedule_agent') {
|
||||
return { tier: 'reminder', path: 'agent', action: sched.action, detail: sched };
|
||||
}
|
||||
|
||||
if (shouldUseScheduledTaskAutomation(text)) {
|
||||
return { tier: 'automation', path: 'agent', action: 'none', detail: schedTask };
|
||||
}
|
||||
|
||||
if (shouldUseScheduleAssistant(text)) {
|
||||
return { tier: 'reminder', path: 'agent', action: sched.action, detail: sched };
|
||||
}
|
||||
|
||||
return { tier: 'none', path: 'general', action: 'none', detail: sched };
|
||||
}
|
||||
|
||||
function tierMatches(expectedTier, actualTier, text) {
|
||||
if (expectedTier === 'ambiguous') return actualTier === 'reminder' || actualTier === 'automation';
|
||||
if (expectedTier === actualTier) return true;
|
||||
if (expectedTier === 'none' && actualTier === 'none') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function pathMatches(expectedPath, actualPath) {
|
||||
if (expectedPath === actualPath) return true;
|
||||
if (expectedPath === 'preflight_or_agent' && (actualPath === 'preflight' || actualPath === 'agent')) return true;
|
||||
if (expectedPath === 'agent_or_clarify' && (actualPath === 'agent' || actualPath === 'clarify')) return true;
|
||||
if (expectedPath === 'clarify' && (actualPath === 'clarify' || actualPath === 'agent')) return true;
|
||||
if (expectedPath === 'general' && actualPath === 'general') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const corpus = buildCorpus();
|
||||
const stats = {
|
||||
total: corpus.length,
|
||||
tierCorrect: 0,
|
||||
pathOk: 0,
|
||||
byTier: {},
|
||||
byPath: {},
|
||||
misroutes: [],
|
||||
clarifyCases: [],
|
||||
agentFallback: [],
|
||||
conflicts: [],
|
||||
};
|
||||
|
||||
for (const item of corpus) {
|
||||
const route = classifyRoute(item.text);
|
||||
stats.byTier[route.tier] = (stats.byTier[route.tier] ?? 0) + 1;
|
||||
stats.byPath[route.path] = (stats.byPath[route.path] ?? 0) + 1;
|
||||
|
||||
const tierOk = tierMatches(item.expectedTier, route.tier, item.text);
|
||||
const pathOk = pathMatches(item.expectedPath ?? 'preflight_or_agent', route.path);
|
||||
|
||||
if (tierOk) stats.tierCorrect += 1;
|
||||
if (pathOk) stats.pathOk += 1;
|
||||
|
||||
if (!tierOk) {
|
||||
stats.misroutes.push({
|
||||
text: item.text,
|
||||
expectedTier: item.expectedTier,
|
||||
actualTier: route.tier,
|
||||
path: route.path,
|
||||
action: route.action,
|
||||
note: item.note ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
if (route.path === 'clarify') {
|
||||
stats.clarifyCases.push({ text: item.text, action: route.action, detail: route.detail });
|
||||
}
|
||||
if (route.path === 'agent') {
|
||||
stats.agentFallback.push({ text: item.text, expectedTier: item.expectedTier, action: route.action });
|
||||
}
|
||||
if (shouldUseScheduledTaskAutomation(item.text) && shouldUseScheduleAssistant(item.text)) {
|
||||
stats.conflicts.push(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
const tierAccuracy = ((stats.tierCorrect / stats.total) * 100).toFixed(1);
|
||||
const pathAccuracy = ((stats.pathOk / stats.total) * 100).toFixed(1);
|
||||
const preflightRate = (
|
||||
((stats.byPath.preflight ?? 0) / stats.total) * 100
|
||||
).toFixed(1);
|
||||
const agentRate = (
|
||||
((stats.byPath.agent ?? 0) / stats.total) * 100
|
||||
).toFixed(1);
|
||||
const clarifyRate = (
|
||||
((stats.byPath.clarify ?? 0) / stats.total) * 100
|
||||
).toFixed(1);
|
||||
|
||||
const misrouteByReason = {};
|
||||
for (const row of stats.misroutes) {
|
||||
const key = `${row.expectedTier}->${row.actualTier}`;
|
||||
misrouteByReason[key] = (misrouteByReason[key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
summary: {
|
||||
total: stats.total,
|
||||
tierAccuracyPct: Number(tierAccuracy),
|
||||
pathAccuracyPct: Number(pathAccuracy),
|
||||
preflightRatePct: Number(preflightRate),
|
||||
agentFallbackRatePct: Number(agentRate),
|
||||
clarifyRatePct: Number(clarifyRate),
|
||||
skillConflicts: stats.conflicts.length,
|
||||
},
|
||||
routeDistribution: stats.byTier,
|
||||
pathDistribution: stats.byPath,
|
||||
misroutePatterns: misrouteByReason,
|
||||
topMisroutes: stats.misroutes.slice(0, 25),
|
||||
sampleAgentFallback: stats.agentFallback.slice(0, 15),
|
||||
sampleClarify: stats.clarifyCases.slice(0, 10),
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user