import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { ownerRoot } from '~/server/utils/library' import { normalizePromptSequence, normalizePromptSequenceName, normalizeCharacter, normalizeSet, normalizeScene, emptyCharacter, emptyScene, type PromptSequence, type PromptSequenceCharacter, type PromptSequenceSet, type PromptSequenceScene } from '~/utils/promptSequences' const writeChains = new Map>() function sequencesPath(owner: string) { return join(ownerRoot(owner), 'prompt-sequences.json') } function ensureOwner(owner: string) { mkdirSync(ownerRoot(owner), { recursive: true }) } function readSequences(owner: string): PromptSequence[] { ensureOwner(owner) const path = sequencesPath(owner) if (!existsSync(path)) return [] try { const parsed = JSON.parse(readFileSync(path, 'utf8')) return Array.isArray(parsed) ? parsed.map(normalizePromptSequence).filter((item): item is PromptSequence => Boolean(item)) : [] } catch { return [] } } function writeSequences(owner: string, sequences: PromptSequence[]) { ensureOwner(owner) const path = sequencesPath(owner) const tmp = `${path}.tmp` writeFileSync(tmp, JSON.stringify(sequences, null, 2)) renameSync(tmp, path) } function mutate(owner: string, fn: (sequences: PromptSequence[]) => T): Promise { const prev = writeChains.get(owner) || Promise.resolve() const run = prev.then(() => { const sequences = readSequences(owner) const result = fn(sequences) writeSequences(owner, sequences) return result }) writeChains.set(owner, run.then(() => undefined, () => undefined)) return run } function normalizeCharactersInput(raw: unknown): PromptSequenceCharacter[] { if (!Array.isArray(raw) || !raw.length) return [emptyCharacter()] return raw.map(normalizeCharacter) } function normalizeSetsInput(raw: unknown): PromptSequenceSet[] { if (!Array.isArray(raw)) return [] return raw.map(normalizeSet) } function normalizeScenesInput( raw: unknown, characters: PromptSequenceCharacter[], sets: PromptSequenceSet[] ): PromptSequenceScene[] { if (!Array.isArray(raw) || !raw.length) return [emptyScene(1, characters, sets)] return raw.map((scene, index) => normalizeScene(scene, index, characters, sets)) } export function listPromptSequences(owner: string) { return readSequences(owner).sort((a, b) => b.updatedAt - a.updatedAt || a.name.localeCompare(b.name)) } export function getPromptSequence(owner: string, id: string) { const wanted = String(id || '').trim() const sequence = readSequences(owner).find(item => item.id === wanted) if (!sequence) throw createError({ statusCode: 404, statusMessage: 'Sequence not found' }) return sequence } export async function savePromptSequence(owner: string, input: { name: string characters?: unknown sets?: unknown scenes?: unknown overwrite?: boolean }) { const name = normalizePromptSequenceName(input.name) if (!name) throw createError({ statusCode: 400, statusMessage: 'A sequence name is required' }) const characters = normalizeCharactersInput(input.characters) const sets = normalizeSetsInput(input.sets) const scenes = normalizeScenesInput(input.scenes, characters, sets) const now = Date.now() return mutate(owner, (sequences) => { const existing = sequences.find(item => item.name.toLowerCase() === name.toLowerCase()) if (existing && !input.overwrite) { throw createError({ statusCode: 409, statusMessage: `Overwrite sequence “${existing.name}”?` }) } if (existing) { existing.characters = characters existing.sets = sets existing.scenes = scenes existing.updatedAt = now return existing } const created: PromptSequence = { id: crypto.randomUUID(), name, characters, sets, scenes, createdAt: now, updatedAt: now } sequences.unshift(created) return created }) } export async function updatePromptSequence(owner: string, id: string, input: { name?: string characters?: unknown sets?: unknown scenes?: unknown }) { const wanted = String(id || '').trim() if (!wanted) throw createError({ statusCode: 400, statusMessage: 'Missing sequence id' }) return mutate(owner, (sequences) => { const existing = sequences.find(item => item.id === wanted) if (!existing) throw createError({ statusCode: 404, statusMessage: 'Sequence not found' }) if (input.name !== undefined) { const name = normalizePromptSequenceName(input.name) if (!name) throw createError({ statusCode: 400, statusMessage: 'A sequence name is required' }) const clash = sequences.find(item => item.id !== wanted && item.name.toLowerCase() === name.toLowerCase()) if (clash) throw createError({ statusCode: 409, statusMessage: `A sequence named “${clash.name}” already exists` }) existing.name = name } if (input.characters !== undefined || input.sets !== undefined || input.scenes !== undefined) { const characters = input.characters !== undefined ? normalizeCharactersInput(input.characters) : existing.characters const sets = input.sets !== undefined ? normalizeSetsInput(input.sets) : (existing.sets || []) const scenes = input.scenes !== undefined ? normalizeScenesInput(input.scenes, characters, sets) : existing.scenes.map((scene, index) => normalizeScene(scene, index, characters, sets)) existing.characters = characters existing.sets = sets existing.scenes = scenes } existing.updatedAt = Date.now() return existing }) } export async function deletePromptSequence(owner: string, id: string) { const wanted = String(id || '').trim() if (!wanted) throw createError({ statusCode: 400, statusMessage: 'Missing sequence id' }) return mutate(owner, (sequences) => { const index = sequences.findIndex(item => item.id === wanted) if (index < 0) throw createError({ statusCode: 404, statusMessage: 'Sequence not found' }) const [removed] = sequences.splice(index, 1) return removed }) }