'use strict'; const LIFECYCLE_EVENT_TYPES = Object.freeze({ USER_MESSAGE_RECEIVED: 'user_message_received', TURN_STARTED: 'turn_started', USER_INPUT_REQUESTED: 'user_input_requested', TURN_COMPLETED: 'turn_completed', TURN_FAILED: 'turn_failed', }); const SUPPORTED_EVENT_TYPES = new Set(Object.values(LIFECYCLE_EVENT_TYPES)); const EVENT_TYPE_ALIASES = Object.freeze({ user_message: LIFECYCLE_EVENT_TYPES.USER_MESSAGE_RECEIVED, message_received: LIFECYCLE_EVENT_TYPES.USER_MESSAGE_RECEIVED, user_message_received: LIFECYCLE_EVENT_TYPES.USER_MESSAGE_RECEIVED, turn_start: LIFECYCLE_EVENT_TYPES.TURN_STARTED, turn_started: LIFECYCLE_EVENT_TYPES.TURN_STARTED, request_user_input: LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED, user_input_request: LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED, user_input_requested: LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED, item_tool_request_user_input: LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED, item_tool_requestuserinput: LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED, turn_complete: LIFECYCLE_EVENT_TYPES.TURN_COMPLETED, turn_completed: LIFECYCLE_EVENT_TYPES.TURN_COMPLETED, turn_error: LIFECYCLE_EVENT_TYPES.TURN_FAILED, turn_failed: LIFECYCLE_EVENT_TYPES.TURN_FAILED, }); function isObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function cleanText(value, maxLength = 4000) { return typeof value === 'string' ? value.trim().slice(0, maxLength) : ''; } function normalizeTimestamp(value, now) { if (value instanceof Date && Number.isFinite(value.getTime())) return value.toISOString(); if (typeof value === 'number' && Number.isFinite(value)) { const parsed = new Date(value); if (Number.isFinite(parsed.getTime())) return parsed.toISOString(); } if (typeof value === 'string' && value.trim()) { const parsed = new Date(value); if (Number.isFinite(parsed.getTime())) return parsed.toISOString(); } return new Date(now()).toISOString(); } function normalizeLifecycleEvent(input, options = {}) { if (!isObject(input)) return null; const now = typeof options.now === 'function' ? options.now : Date.now; const rawType = cleanText(input.type || input.eventType || input.event, 80).toLowerCase(); let type = EVENT_TYPE_ALIASES[rawType] || rawType; const outcome = cleanText(input.outcome || input.status, 80).toLowerCase(); if (type === LIFECYCLE_EVENT_TYPES.TURN_COMPLETED && /^(failed|failure|error|errored)$/.test(outcome)) { type = LIFECYCLE_EVENT_TYPES.TURN_FAILED; } if (!SUPPORTED_EVENT_TYPES.has(type)) { return { valid: false, reason: 'unsupported_event', type: type || null }; } const errorValue = input.error; const error = typeof errorValue === 'string' ? cleanText(errorValue) : (isObject(errorValue) ? cleanText(errorValue.message) : ''); return { valid: true, type, turnId: cleanText(input.turnId || input.turn_id, 128) || null, eventId: cleanText(input.eventId || input.event_id || input.id, 160) || null, occurredAt: normalizeTimestamp(input.occurredAt || input.timestamp, now), outcome, pendingUserInput: input.pendingUserInput === true, structured: input.structured !== false, trackingEnabled: input.trackingEnabled, error: error || null, }; } function ignoredResult(sessionId, event, reason, extra = {}) { return { ok: true, handled: false, changed: false, ignored: true, reason, sessionId, eventType: event?.type || null, ...extra, }; } function normalizeHandleArguments(sessionOrEnvelope, maybeEvent) { if (typeof sessionOrEnvelope === 'string') { return { sessionId: cleanText(sessionOrEnvelope, 128), event: maybeEvent }; } if (!isObject(sessionOrEnvelope)) return { sessionId: '', event: maybeEvent }; const sessionId = cleanText( sessionOrEnvelope.sessionId || sessionOrEnvelope.conversationId || sessionOrEnvelope.session_id, 128, ); return { sessionId, event: isObject(sessionOrEnvelope.event) ? sessionOrEnvelope.event : sessionOrEnvelope, }; } function trackingEnabledFrom(value) { if (typeof value === 'boolean') return value; if (!isObject(value)) return null; if (typeof value.enabled === 'boolean') return value.enabled; if (typeof value.trackingEnabled === 'boolean') return value.trackingEnabled; if (typeof value.taskTracking?.enabled === 'boolean') return value.taskTracking.enabled; if (typeof value.task?.taskTracking?.enabled === 'boolean') return value.task.taskTracking.enabled; return null; } function serviceEventFrom(event) { const output = { type: event.type, occurredAt: event.occurredAt, }; if (event.turnId) output.turnId = event.turnId; if (event.outcome) output.outcome = event.outcome; if (event.pendingUserInput) output.pendingUserInput = true; if (event.error) output.error = event.error; return output; } function eventIdentity(event) { if (event.eventId) return `event:${event.eventId}`; if (event.turnId) return `turn:${event.turnId}:${event.type}`; return ''; } function createTaskBoardLifecycle(deps = {}, extraOptions = {}) { const directService = isObject(deps) && typeof deps.recordLifecycleEvent === 'function' ? deps : null; const options = directService ? (isObject(extraOptions) ? extraOptions : {}) : { ...(isObject(deps) ? deps : {}), ...(isObject(extraOptions) ? extraOptions : {}) }; const taskBoardService = directService || options.taskBoardService || options.service; if (!taskBoardService || typeof taskBoardService.recordLifecycleEvent !== 'function') { throw new TypeError('createTaskBoardLifecycle 需要注入 TaskBoardService.recordLifecycleEvent'); } const now = typeof options.now === 'function' ? options.now : Date.now; const trackingChecker = typeof options.isTrackingEnabled === 'function' ? options.isTrackingEnabled : null; const logger = options.logger || null; const queues = new Map(); const identitiesBySession = new Map(); function enqueue(sessionId, operation) { const previous = queues.get(sessionId) || Promise.resolve(); const next = previous.catch(() => undefined).then(operation); queues.set(sessionId, next); return next.finally(() => { if (queues.get(sessionId) === next) queues.delete(sessionId); }); } function hasIdentity(sessionId, identity) { return Boolean(identity && identitiesBySession.get(sessionId)?.has(identity)); } function rememberIdentity(sessionId, identity) { if (!identity) return; let identities = identitiesBySession.get(sessionId); if (!identities) { identities = new Set(); identitiesBySession.set(sessionId, identities); } identities.add(identity); while (identities.size > 256) identities.delete(identities.values().next().value); } function logFailure(error, sessionId, event) { if (!logger) return; const payload = { sessionId, eventType: event?.type || '', error: cleanText(error?.message || error) }; if (typeof logger === 'function') logger('error', 'task_board_lifecycle_failed', payload); else if (typeof logger.error === 'function') logger.error('task_board_lifecycle_failed', payload); } async function processEvent(sessionId, input) { const event = normalizeLifecycleEvent(input, { now }); if (!event) return ignoredResult(sessionId, null, 'invalid_event'); if (!event.valid) return ignoredResult(sessionId, event, event.reason); if (event.trackingEnabled === false) return ignoredResult(sessionId, event, 'task_tracking_disabled'); if (event.type === LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED && !event.structured) { return ignoredResult(sessionId, event, 'unstructured_user_input'); } if (trackingChecker) { const enabled = trackingEnabledFrom(await trackingChecker(sessionId, event)); if (enabled === false) return ignoredResult(sessionId, event, 'task_tracking_disabled'); } const identity = eventIdentity(event); if (hasIdentity(sessionId, identity)) { return ignoredResult(sessionId, event, 'duplicate_event'); } let serviceResult; try { serviceResult = await taskBoardService.recordLifecycleEvent(sessionId, serviceEventFrom(event)); } catch (error) { if (error?.code === 'task_tracking_disabled') { return ignoredResult(sessionId, event, 'task_tracking_disabled'); } logFailure(error, sessionId, event); throw error; } rememberIdentity(sessionId, identity); return { ok: true, handled: serviceResult?.ignored !== true, changed: serviceResult?.changed === true, ignored: serviceResult?.ignored === true, reason: serviceResult?.reason || null, sessionId, eventType: event.type, ...(serviceResult?.task ? { task: serviceResult.task } : {}), }; } function handleEvent(sessionOrEnvelope, maybeEvent) { const { sessionId, event } = normalizeHandleArguments(sessionOrEnvelope, maybeEvent); if (!sessionId) return Promise.resolve(ignoredResult('', null, 'session_id_missing')); return enqueue(sessionId, () => processEvent(sessionId, event)); } function clearSession(sessionIdValue) { const sessionId = cleanText(sessionIdValue, 128); if (!sessionId) return Promise.resolve(false); identitiesBySession.delete(sessionId); return Promise.resolve(true); } const eventHandler = (type) => (sessionId, event = {}) => handleEvent(sessionId, { ...event, type }); return Object.freeze({ handleEvent, recordLifecycleEvent: handleEvent, clearSession, onUserMessageReceived: eventHandler(LIFECYCLE_EVENT_TYPES.USER_MESSAGE_RECEIVED), onTurnStarted: eventHandler(LIFECYCLE_EVENT_TYPES.TURN_STARTED), onUserInputRequested: eventHandler(LIFECYCLE_EVENT_TYPES.USER_INPUT_REQUESTED), onTurnCompleted: eventHandler(LIFECYCLE_EVENT_TYPES.TURN_COMPLETED), onTurnFailed: eventHandler(LIFECYCLE_EVENT_TYPES.TURN_FAILED), }); } module.exports = { LIFECYCLE_EVENT_TYPES, createTaskBoardLifecycle, normalizeLifecycleEvent, };