import { db } from '../db/client.js'; const AUTOMATION_CONTEXTS_TABLE = 'automation_contexts'; export type AutomationContextRecord = { id: string; title: string; content: string; enabled: boolean; defaultSelected: boolean; updatedAt: string; }; export const DEFAULT_AUTOMATION_CONTEXTS: AutomationContextRecord[] = [ { id: 'general-inquiry-default', title: '기본 확인', content: '소스 수정 없이 조회, 상태 확인, 응답 정리를 우선합니다.', enabled: true, defaultSelected: true, updatedAt: '2026-04-29T00:00:00.000Z', }, { id: 'none-default', title: '기본 처리', content: '## 기본 처리\n- 현재 저장소 규칙과 요청 본문을 우선 따릅니다.\n- 소스 수정이 필요하면 관련 파일을 수정하고, 필요 없으면 확인 결과만 정리합니다.\n- 작업 결과에는 변경 파일, diff, preview 같은 증적을 남길 수 있습니다.', enabled: true, defaultSelected: true, updatedAt: '2026-04-29T00:00:00.000Z', }, { id: 'plan-default', title: '문서형 처리', content: 'Plan 등록/문서형 요청으로 처리하고, 구현보다 등록/정리 결과를 우선 남깁니다.', enabled: true, defaultSelected: false, updatedAt: '2026-04-29T00:00:00.000Z', }, { id: 'command-execution-default', title: '명령 실행', content: '저장소 수정 없이 명령 실행, API 확인, 상태 조회 중심으로 처리합니다.', enabled: true, defaultSelected: false, updatedAt: '2026-04-29T00:00:00.000Z', }, { id: 'non-source-work-default', title: '비소스 작업', content: '문서, 운영, 데이터 확인 등 비소스 작업으로 처리하고 코드 수정은 요청 시에만 제한적으로 수행합니다.', enabled: true, defaultSelected: false, updatedAt: '2026-04-29T00:00:00.000Z', }, { id: 'auto-worker-default', title: '자동화 기본 규칙', content: '## context 사용 규칙\n- 자동화 실행기는 선택된 Context만 우선 참조합니다.\n- Codex Live 문맥이나 일반 채팅 문맥은 자동화 기본 context로 섞지 않습니다.\n\n## 처리 원칙\n- 세부 절차는 현재 운영 설정을 따릅니다.', enabled: true, defaultSelected: true, updatedAt: '2026-04-29T00:00:00.000Z', }, ]; function normalizeText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function normalizeEnabled(value: unknown) { if (typeof value === 'boolean') { return value; } if (typeof value === 'number') { return value !== 0; } if (typeof value === 'string') { const normalizedValue = value.trim().toLowerCase(); if (['false', '0', 'n', 'no', 'off'].includes(normalizedValue)) { return false; } if (['true', '1', 'y', 'yes', 'on'].includes(normalizedValue)) { return true; } } return value !== false; } function buildContextTitleKey(value: string) { return value.replace(/\s+/g, ' ').toLocaleLowerCase('ko-KR'); } function compareContextUpdatedAt(left: AutomationContextRecord, right: AutomationContextRecord) { const leftTime = Date.parse(left.updatedAt); const rightTime = Date.parse(right.updatedAt); if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) { return leftTime - rightTime; } return 0; } function normalizeAutomationContext(record: Partial): AutomationContextRecord | null { const title = normalizeText(record.title); const content = normalizeText(record.content); if (!title && !content) { return null; } const rawId = normalizeText(record.id); const normalizedId = rawId || `automation-context-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; return { id: normalizedId, title: title || 'Context', content, enabled: normalizeEnabled(record.enabled), defaultSelected: normalizeEnabled(record.defaultSelected), updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(), }; } export function sanitizeAutomationContexts(items: Partial[] | null | undefined) { const byId = new Map(); const bySemanticKey = new Map(); (items ?? []) .map((item) => normalizeAutomationContext(item)) .filter((item): item is AutomationContextRecord => Boolean(item)) .forEach((item) => { const currentById = byId.get(item.id); if (!currentById || compareContextUpdatedAt(currentById, item) <= 0) { byId.set(item.id, item); } }); for (const item of byId.values()) { const semanticKey = buildContextTitleKey(item.title); const current = bySemanticKey.get(semanticKey); if (!current || compareContextUpdatedAt(current, item) <= 0) { bySemanticKey.set(semanticKey, item); } } const values = Array.from(bySemanticKey.values()).sort((left, right) => left.title.localeCompare(right.title, 'ko-KR')); return values.length > 0 ? values : DEFAULT_AUTOMATION_CONTEXTS; } async function ensureAutomationContextsTable() { const hasTable = await db.schema.hasTable(AUTOMATION_CONTEXTS_TABLE); if (!hasTable) { await db.schema.createTable(AUTOMATION_CONTEXTS_TABLE, (table) => { table.string('id').primary(); table.string('title').notNullable(); table.text('content').notNullable().defaultTo(''); table.boolean('enabled').notNullable().defaultTo(true); table.boolean('default_selected').notNullable().defaultTo(false); table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now()); }); return; } const requiredColumns: Array<[string, (table: any) => void]> = [ ['title', (table) => table.string('title').notNullable().defaultTo('')], ['content', (table) => table.text('content').notNullable().defaultTo('')], ['enabled', (table) => table.boolean('enabled').notNullable().defaultTo(true)], ['default_selected', (table) => table.boolean('default_selected').notNullable().defaultTo(false)], ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())], ]; for (const [columnName, createColumn] of requiredColumns) { const hasColumn = await db.schema.hasColumn(AUTOMATION_CONTEXTS_TABLE, columnName); if (!hasColumn) { await db.schema.alterTable(AUTOMATION_CONTEXTS_TABLE, (table) => { createColumn(table); }); } } } function parseContextsFromLegacyValue(value: unknown) { if (typeof value !== 'string') { return []; } try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function toAutomationContextRecord(row: Record) { return normalizeAutomationContext({ id: typeof row.id === 'string' ? row.id : undefined, title: typeof row.title === 'string' ? row.title : undefined, content: typeof row.content === 'string' ? row.content : undefined, enabled: normalizeEnabled(row.enabled), defaultSelected: normalizeEnabled(row.default_selected), updatedAt: typeof row.updated_at === 'string' ? row.updated_at : undefined, }); } async function replaceAutomationContextsInTable(items: AutomationContextRecord[]) { await ensureAutomationContextsTable(); const nextItems = sanitizeAutomationContexts(items); await db.transaction(async (trx) => { await trx(AUTOMATION_CONTEXTS_TABLE).del(); await trx(AUTOMATION_CONTEXTS_TABLE).insert( nextItems.map((item) => ({ id: item.id, title: item.title, content: item.content, enabled: item.enabled, default_selected: item.defaultSelected, updated_at: item.updatedAt, })), ); }); return nextItems; } async function seedAutomationContextsFromLegacySources() { const seededItems: Partial[] = [...DEFAULT_AUTOMATION_CONTEXTS]; const hasAutomationTypesTable = await db.schema.hasTable('automation_types'); if (hasAutomationTypesTable) { const rows = await db('automation_types').select('contexts_json'); for (const row of rows) { seededItems.push(...parseContextsFromLegacyValue((row as Record).contexts_json)); } } return replaceAutomationContextsInTable(sanitizeAutomationContexts(seededItems)); } function isSameAutomationContextList(left: AutomationContextRecord[], right: AutomationContextRecord[]) { if (left.length !== right.length) { return false; } return left.every((item, index) => { const target = right[index]; return ( target && item.id === target.id && item.title === target.title && item.content === target.content && item.enabled === target.enabled && item.defaultSelected === target.defaultSelected && item.updatedAt === target.updatedAt ); }); } function mergeDefaultAutomationContexts(items: AutomationContextRecord[]) { const byId = new Map(items.map((item) => [item.id, item] as const)); for (const defaultItem of DEFAULT_AUTOMATION_CONTEXTS) { const existingItem = byId.get(defaultItem.id); if (!existingItem) { byId.set(defaultItem.id, defaultItem); continue; } byId.set(defaultItem.id, { ...existingItem, title: defaultItem.title, content: existingItem.content || defaultItem.content, }); } return sanitizeAutomationContexts(Array.from(byId.values())); } async function readAutomationContextsFromTable() { await ensureAutomationContextsTable(); const rows = await db(AUTOMATION_CONTEXTS_TABLE) .select('id', 'title', 'content', 'enabled', 'default_selected', 'updated_at') .orderBy('title', 'asc'); const savedItems = rows .map((row) => toAutomationContextRecord(row as Record)) .filter((item): item is AutomationContextRecord => Boolean(item)); return sanitizeAutomationContexts(savedItems); } export async function getAutomationContextsConfig() { const savedContexts = await readAutomationContextsFromTable(); if (savedContexts.length === 0 || savedContexts === DEFAULT_AUTOMATION_CONTEXTS) { return seedAutomationContextsFromLegacySources(); } const mergedContexts = mergeDefaultAutomationContexts(savedContexts); if (!isSameAutomationContextList(savedContexts, mergedContexts)) { await replaceAutomationContextsInTable(mergedContexts); } return mergedContexts; } export async function upsertAutomationContextsConfig(items: unknown[]) { const nextContexts = mergeDefaultAutomationContexts( sanitizeAutomationContexts(Array.isArray(items) ? (items as Partial[]) : []), ); return replaceAutomationContextsInTable(nextContexts); } export function normalizeAutomationContextSelection(value: unknown) { const rawValues = Array.isArray(value) ? value : typeof value === 'string' ? value .split(',') .map((item) => item.trim()) .filter(Boolean) : []; return [...new Set(rawValues.map((item) => normalizeText(String(item))).filter(Boolean))]; } export function resolveAutomationContexts( contexts: AutomationContextRecord[] | null | undefined, selectedContextIds?: unknown, ) { const normalizedContexts = sanitizeAutomationContexts(contexts); const requestedIds = normalizeAutomationContextSelection(selectedContextIds); if (requestedIds.length === 0 && Array.isArray(selectedContextIds)) { return []; } if (requestedIds.length === 0) { return normalizedContexts.filter((item) => item.enabled && item.defaultSelected); } const requestedIdSet = new Set(requestedIds); return normalizedContexts.filter((item) => requestedIdSet.has(item.id)); }