import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { eq, sql } from 'drizzle-orm' import { z } from 'zod' const optionsSchema = z.object({ /** 把卡片的 scenario 追加到故事背景 */ scenarioToWorld: z.boolean().default(true), /** 导入角色卡内置世界书 */ importLore: z.boolean().default(true), /** 把开场白(first_mes)作为文风/说话示例 */ firstMesAsSample: z.boolean().default(true), /** 把卡片的 system_prompt 写入角色备注 */ systemPromptToNotes: z.boolean().default(false), /** {{user}} 的替换名,留空则使用项目设置 */ userName: z.string().default(''), /** 角色身份标签,如“女主角” */ identity: z.string().default('') }) export default defineEventHandler(async (event) => { const projectId = param(event) const project = getProject(projectId) const form = await readMultipartFormData(event) const file = form?.find(f => f.name === 'file') if (!file?.data?.length) throw createError({ statusCode: 400, message: '请上传角色卡文件' }) const optRaw = form?.find(f => f.name === 'options')?.data.toString('utf8') || '{}' const opts = optionsSchema.parse(JSON.parse(optRaw)) let card try { card = parseCardFile(file.data, file.filename) } catch (err) { throw createError({ statusCode: 400, message: (err as Error).message }) } const userName = opts.userName || project.userName const m = (s: string) => replaceMacros(s, card.name, userName).trim() const db = useDb() const charId = newId() const samples: string[] = [] if (card.mesExample.trim()) samples.push(m(card.mesExample)) if (opts.firstMesAsSample && card.firstMes.trim()) samples.push(`【开场白示例】\n${m(card.firstMes)}`) const notes: string[] = [] if (!opts.scenarioToWorld && card.scenario.trim()) notes.push(`【场景】${m(card.scenario)}`) if (opts.systemPromptToNotes && card.systemPrompt.trim()) notes.push(`【卡片系统提示】${m(card.systemPrompt)}`) if (card.postHistoryInstructions.trim() && opts.systemPromptToNotes) notes.push(`【补充指令】${m(card.postHistoryInstructions)}`) let avatar = '' if (file.data[0] === 0x89) { const dir = join(projectDir(projectId), 'avatars') mkdirSync(dir, { recursive: true }) avatar = `${charId}.png` writeFileSync(join(dir, avatar), file.data) } const count = db.select({ n: sql`count(*)` }).from(tables.characters).where(eq(tables.characters.projectId, projectId)).get()?.n ?? 0 db.transaction((tx) => { tx.insert(tables.characters).values({ id: charId, projectId, name: card.name, identity: opts.identity, description: m(card.description), personality: m(card.personality), speechExamples: samples.join('\n\n'), notes: notes.join('\n\n'), avatar, sourceCard: card.raw, sortOrder: count }).run() if (opts.importLore && card.lore.length) { tx.insert(tables.loreEntries).values(card.lore.map(l => ({ id: newId(), projectId, title: `${card.name}·${l.title}`, keys: l.keys, content: m(l.content), constant: l.constant, enabled: l.enabled, source: 'card' as const, sortOrder: l.order }))).run() } if (opts.scenarioToWorld && card.scenario.trim()) { const world = [project.worldSetting.trim(), `【${card.name}·场景】\n${m(card.scenario)}`].filter(Boolean).join('\n\n') tx.update(tables.projects).set({ worldSetting: world }).where(eq(tables.projects.id, projectId)).run() } }) return { id: charId, name: card.name, loreCount: opts.importLore ? card.lore.length : 0 } })