Files
memind/task-unified-sync.mjs
T
john d58dc2a251 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>
2026-08-24 15:59:17 +08:00

184 lines
6.1 KiB
JavaScript

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 };
}