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,183 @@
|
||||
import { isUnifiedTasksEnabled } from './task-unified-config.mjs';
|
||||
import { mapLegacyTodoItemRow } from './task-unified-service.mjs';
|
||||
|
||||
function syncOpen(taskUnifiedService, fn) {
|
||||
return fn().catch((err) => {
|
||||
console.warn?.(
|
||||
'[task-unified-sync] failed open:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function attachUnifiedTaskSync({
|
||||
scheduleService,
|
||||
scheduledTaskService,
|
||||
taskUnifiedService,
|
||||
pool,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
}) {
|
||||
if (!isUnifiedTasksEnabled(env) || !taskUnifiedService || !pool) {
|
||||
return { scheduleService, scheduledTaskService };
|
||||
}
|
||||
|
||||
const warn = (...args) => logger.warn?.(...args);
|
||||
|
||||
if (scheduleService?.createItem) {
|
||||
const original = scheduleService.createItem.bind(scheduleService);
|
||||
scheduleService.createItem = async (payload) => {
|
||||
const item = await original(payload);
|
||||
if (item?.kind === 'task') {
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.syncFromLegacyMapped(mapLegacyTodoItemRow({
|
||||
id: item.id,
|
||||
user_id: item.userId,
|
||||
title: item.title,
|
||||
timezone: item.timezone,
|
||||
status: item.status,
|
||||
source_channel: payload.sourceChannel ?? null,
|
||||
source_message_id: payload.sourceMessageId ?? null,
|
||||
source_text: payload.sourceText ?? null,
|
||||
})));
|
||||
}
|
||||
return item;
|
||||
};
|
||||
}
|
||||
|
||||
if (scheduleService?.createReminder) {
|
||||
const original = scheduleService.createReminder.bind(scheduleService);
|
||||
scheduleService.createReminder = async (payload) => {
|
||||
const reminder = await original(payload);
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT i.*, r.id AS reminder_id, r.remind_at, r.channel, r.status AS reminder_status
|
||||
FROM h5_schedule_items i
|
||||
JOIN h5_schedule_reminders r ON r.item_id = i.id
|
||||
WHERE r.id = ?
|
||||
LIMIT 1`,
|
||||
[reminder.id],
|
||||
);
|
||||
const row = rows?.[0];
|
||||
if (row) {
|
||||
await taskUnifiedService.syncFromCommit({
|
||||
userId: payload.userId,
|
||||
kind: 'timed_reminder',
|
||||
committed: {
|
||||
item: {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
startAt: row.start_at ?? reminder.remindAt,
|
||||
timezone: row.timezone,
|
||||
sourceChannel: row.source_channel,
|
||||
sourceMessageId: row.source_message_id,
|
||||
sourceText: row.source_text,
|
||||
},
|
||||
reminder,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
warn('[task-unified-sync] reminder sync failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
return reminder;
|
||||
};
|
||||
}
|
||||
|
||||
if (scheduleService?.createDailyTodoDigest) {
|
||||
const original = scheduleService.createDailyTodoDigest.bind(scheduleService);
|
||||
scheduleService.createDailyTodoDigest = async (payload) => {
|
||||
const subscription = await original(payload);
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.syncFromCommit({
|
||||
userId: payload.userId,
|
||||
kind: 'create_daily_todo_digest',
|
||||
committed: { subscription },
|
||||
}));
|
||||
return subscription;
|
||||
};
|
||||
}
|
||||
|
||||
if (scheduleService?.createBalanceLowAlert) {
|
||||
const original = scheduleService.createBalanceLowAlert.bind(scheduleService);
|
||||
scheduleService.createBalanceLowAlert = async (payload) => {
|
||||
const subscription = await original(payload);
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.syncFromCommit({
|
||||
userId: payload.userId,
|
||||
kind: 'create_balance_alert',
|
||||
committed: { subscription },
|
||||
}));
|
||||
return subscription;
|
||||
};
|
||||
}
|
||||
|
||||
if (scheduledTaskService?.createTask) {
|
||||
const original = scheduledTaskService.createTask.bind(scheduledTaskService);
|
||||
scheduledTaskService.createTask = async (payload) => {
|
||||
const task = await original(payload);
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.syncFromCommit({
|
||||
userId: payload.userId,
|
||||
kind: 'scheduled_task',
|
||||
committed: { task },
|
||||
}));
|
||||
return task;
|
||||
};
|
||||
}
|
||||
|
||||
if (scheduledTaskService?.cancelTask) {
|
||||
const original = scheduledTaskService.cancelTask.bind(scheduledTaskService);
|
||||
scheduledTaskService.cancelTask = async (payload) => {
|
||||
const task = await original(payload);
|
||||
if (task?.id && payload?.userId) {
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.markLegacyCancelled({
|
||||
userId: payload.userId,
|
||||
legacyRef: { table: 'h5_scheduled_tasks', id: task.id },
|
||||
}));
|
||||
}
|
||||
return task;
|
||||
};
|
||||
}
|
||||
|
||||
const automationLegacyRef = (task) => ({
|
||||
table: 'h5_scheduled_tasks',
|
||||
id: task.id,
|
||||
});
|
||||
|
||||
if (scheduledTaskService?.markTaskSucceeded) {
|
||||
const original = scheduledTaskService.markTaskSucceeded.bind(scheduledTaskService);
|
||||
scheduledTaskService.markTaskSucceeded = async (task, options) => {
|
||||
const updated = await original(task, options);
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.syncLegacyLifecycle({
|
||||
userId: updated.userId,
|
||||
legacyRef: automationLegacyRef(updated),
|
||||
status: updated.status === 'completed' ? 'completed' : 'active',
|
||||
nextRunAt: updated.nextRunAt ?? null,
|
||||
lastRunAt: updated.lastRunAt ?? null,
|
||||
lastError: null,
|
||||
}));
|
||||
return updated;
|
||||
};
|
||||
}
|
||||
|
||||
if (scheduledTaskService?.markTaskFailed) {
|
||||
const original = scheduledTaskService.markTaskFailed.bind(scheduledTaskService);
|
||||
scheduledTaskService.markTaskFailed = async (task, error, options) => {
|
||||
const updated = await original(task, error, options);
|
||||
await syncOpen(taskUnifiedService, () =>
|
||||
taskUnifiedService.syncLegacyLifecycle({
|
||||
userId: updated.userId,
|
||||
legacyRef: automationLegacyRef(updated),
|
||||
status: updated.status === 'failed' ? 'failed' : 'active',
|
||||
nextRunAt: updated.nextRunAt ?? null,
|
||||
lastError: updated.lastError ?? null,
|
||||
}));
|
||||
return updated;
|
||||
};
|
||||
}
|
||||
|
||||
return { scheduleService, scheduledTaskService };
|
||||
}
|
||||
Reference in New Issue
Block a user