import crypto from 'node:crypto'; import { intentDraftTtlMs } from './intent-transaction-config.mjs'; function nowMs(clock) { return clock.now(); } function rowToDraft(row) { if (!row) return null; return { id: row.id, userId: row.user_id, layer: row.layer, draftType: row.draft_type, actionLevel: Number(row.action_level ?? 1), title: row.title, payload: parseJson(row.payload_json), cardText: row.card_text, status: row.status, sourceChannel: row.source_channel, sourceMessageId: row.source_message_id ?? null, sourceText: row.source_text ?? null, committedRef: parseJson(row.committed_ref_json), eventLog: parseJson(row.event_log_json) ?? [], expiresAt: row.expires_at == null ? null : Number(row.expires_at), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), }; } function parseJson(value) { if (value == null || value === '') return null; if (typeof value === 'object') return value; try { return JSON.parse(value); } catch { return null; } } export function createIntentDraftService(pool, { clock = { now: () => Date.now() }, ttlMs = intentDraftTtlMs() } = {}) { if (!pool) throw new Error('缺少数据库连接'); const appendEvent = (events, type, detail = {}) => [ ...(Array.isArray(events) ? events : []), { type, at: clock.now(), ...detail }, ]; const expireStaleDrafts = async (userId) => { const now = nowMs(clock); await pool.query( `UPDATE h5_intent_drafts SET status = 'expired', updated_at = ? WHERE user_id = ? AND status = 'draft' AND expires_at IS NOT NULL AND expires_at <= ?`, [now, userId, now], ); }; const getPendingDraft = async (userId) => { if (!userId) return null; await expireStaleDrafts(userId); const [rows] = await pool.query( `SELECT * FROM h5_intent_drafts WHERE user_id = ? AND status = 'draft' ORDER BY created_at DESC LIMIT 1`, [userId], ); return rowToDraft(rows?.[0]); }; const createDraft = async ({ userId, layer, draftType, actionLevel = 1, title, payload, cardText, sourceChannel = 'wechat', sourceMessageId = null, sourceText = null, }) => { if (!userId) throw new Error('缺少用户'); const safeTitle = String(title ?? '').trim() || '待确认任务'; const now = nowMs(clock); await expireStaleDrafts(userId); await pool.query( `UPDATE h5_intent_drafts SET status = 'cancelled', updated_at = ? WHERE user_id = ? AND status = 'draft'`, [now, userId], ); const id = crypto.randomUUID(); const expiresAt = now + ttlMs; const eventLog = appendEvent([], 'draft_created', { layer, draftType }); await pool.query( `INSERT INTO h5_intent_drafts (id, user_id, layer, draft_type, action_level, title, payload_json, card_text, status, source_channel, source_message_id, source_text, committed_ref_json, event_log_json, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, NULL, ?, ?, ?, ?)`, [ id, userId, layer, draftType, actionLevel, safeTitle, JSON.stringify(payload ?? {}), cardText, sourceChannel, sourceMessageId, sourceText, JSON.stringify(eventLog), expiresAt, now, now, ], ); const [rows] = await pool.query(`SELECT * FROM h5_intent_drafts WHERE id = ? LIMIT 1`, [id]); return rowToDraft(rows?.[0]); }; const cancelDraft = async (draftId, userId, { reason = 'user_cancel' } = {}) => { const now = nowMs(clock); const [rows] = await pool.query( `SELECT * FROM h5_intent_drafts WHERE id = ? AND user_id = ? LIMIT 1`, [draftId, userId], ); const row = rows?.[0]; if (!row) throw new Error('草稿不存在'); if (row.status !== 'draft') return rowToDraft(row); const eventLog = appendEvent(parseJson(row.event_log_json), 'draft_cancelled', { reason }); await pool.query( `UPDATE h5_intent_drafts SET status = 'cancelled', event_log_json = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(eventLog), now, draftId], ); const [updated] = await pool.query(`SELECT * FROM h5_intent_drafts WHERE id = ? LIMIT 1`, [draftId]); return rowToDraft(updated?.[0]); }; const markDraftCommitted = async (draftId, userId, committedRef = {}) => { const now = nowMs(clock); const [rows] = await pool.query( `SELECT * FROM h5_intent_drafts WHERE id = ? AND user_id = ? LIMIT 1`, [draftId, userId], ); const row = rows?.[0]; if (!row) throw new Error('草稿不存在'); const eventLog = appendEvent(parseJson(row.event_log_json), 'draft_committed', { committedRef }); await pool.query( `UPDATE h5_intent_drafts SET status = 'committed', committed_ref_json = ?, event_log_json = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(committedRef), JSON.stringify(eventLog), now, draftId], ); const [updated] = await pool.query(`SELECT * FROM h5_intent_drafts WHERE id = ? LIMIT 1`, [draftId]); return rowToDraft(updated?.[0]); }; return { getPendingDraft, createDraft, cancelDraft, markDraftCommitted, }; }