import { prisma } from "@kokorone/db"; const API_PORT = process.env.PORT_API ?? "3001"; const GENKI_ID = "seed-character-genki"; const COLD_ID = "smoke-g-coldtype"; const USER_ID = "seed-user-primary"; async function get(path) { const res = await fetch(`http://localhost:${API_PORT}${path}`); if (!res.ok) { throw new Error(`GET ${path} 回傳 ${res.status}`); } return res.json(); } async function post(path, body) { const res = await fetch(`http://localhost:${API_PORT}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body ?? {}), }); if (!res.ok) { throw new Error(`POST ${path} 回傳 ${res.status}:${await res.text()}`); } return res.json(); } async function resetEmotion(characterId, overrides) { await prisma.emotionState.upsert({ where: { characterId }, update: { ...overrides, updatedAt: new Date() }, create: { characterId, ...overrides }, }); } export default async function smokeG() { await prisma.character.deleteMany({ where: { id: COLD_ID } }); // G-5 角色建立流程:由角色設定表產生四類初始資料 const coldCharacter = await post("/personality/characters", { id: COLD_ID, source: "ORIGINAL", buildStatus: "BUILT", formalName: "冰璃", basicInfo: "冰璃,16歲,轉學生,總是面無表情", backgroundStory: "小時候因為一次意外失去了摯友,從此學會了不輕易表露情緒,把自己封閉起來保護自己", personalityArchetype: "冷淡", likesDislikes: "喜歡:安靜、看書;討厭:吵鬧", goalsObsessions: "想找到一個可以真心相待的人", speechStyle: "第一人稱「我」,語氣平淡簡短", initialRelationValue: "對誰都保持距離", initialRelationship: { userId: USER_ID, intimacy: 25, trust: 10 }, }); if (coldCharacter.personalityArchetype !== "冷淡") { throw new Error("角色建立未正確寫入性格原型"); } const createdCharacterData = await prisma.character.findUnique({ where: { id: COLD_ID }, include: { semanticMemories: true, episodicMemories: true, emotionState: true, relationships: true }, }); if (createdCharacterData.semanticMemories.length === 0) { throw new Error("G-5 未產生初始語意記憶"); } if (createdCharacterData.episodicMemories.length === 0) { throw new Error("G-5 未產生初始情節記憶"); } if (!createdCharacterData.emotionState) { throw new Error("G-5 未產生初始情緒參數"); } if (createdCharacterData.relationships.length === 0) { throw new Error("G-5 未產生初始關係值"); } // G-1 性格原型參數表:切換原型後同一輸入產生不同情緒(易觸發 vs 難觸發) await resetEmotion(GENKI_ID, { calm: 100, joy: 0, sad: 0, alert: 0, shy: 0, grumpy: 0 }); await resetEmotion(COLD_ID, { calm: 100, joy: 0, sad: 0, alert: 0, shy: 0, grumpy: 0 }); const genkiResult = await post(`/emotion/${GENKI_ID}/input`, { text: "有點難過" }); const coldResult = await post(`/emotion/${COLD_ID}/input`, { text: "有點難過" }); if (genkiResult.state.sad <= 0) { throw new Error("元氣(易觸發)對弱負向輸入應觸發低落情緒"); } if (coldResult.state.sad !== 0) { throw new Error("冷淡(難觸發)對同樣的弱負向輸入不應觸發情緒轉移"); } // G-2 語言風格層:稱呼系統依親密度分層,跨階時 justEvolved 為真 const formalAddress = await get(`/personality/address?userName=${encodeURIComponent("陳大文")}&intimacy=5`); const nameAddress = await get(`/personality/address?userName=${encodeURIComponent("陳大文")}&intimacy=30`); const nicknameAddress = await get(`/personality/address?userName=${encodeURIComponent("陳大文")}&intimacy=70`); if (formalAddress.address !== "您" || nameAddress.address !== "陳大文" || nicknameAddress.address === "陳大文") { throw new Error("稱呼系統未依親密度分層正確切換"); } const transition = await get( `/personality/address-transition?userName=${encodeURIComponent("陳大文")}&previous=15¤t=25`, ); if (!transition.justEvolved) { throw new Error("跨越稱呼分層門檻時應標記 justEvolved"); } // G-3 外顯函數:傲嬌角色內部親密度上升時,外顯敵意(傲嬌值)應先升後降 const gapAt30 = (await get(`/personality/${encodeURIComponent("傲嬌")}/expressed-intimacy?intimacy=30`)).hostilityGap; const gapAt50 = (await get(`/personality/${encodeURIComponent("傲嬌")}/expressed-intimacy?intimacy=50`)).hostilityGap; const gapAt90 = (await get(`/personality/${encodeURIComponent("傲嬌")}/expressed-intimacy?intimacy=90`)).hostilityGap; if (!(gapAt30 < gapAt50 && gapAt50 > gapAt90)) { throw new Error(`傲嬌值應先升後降,實際:30→${gapAt30}, 50→${gapAt50}, 90→${gapAt90}`); } const nonTsundereGap = (await get(`/personality/${encodeURIComponent("元氣")}/expressed-intimacy?intimacy=50`)) .hostilityGap; if (nonTsundereGap !== 0) { throw new Error("非傲嬌原型不應有外顯落差"); } // G-4 反差萌與稀有度控制:門檻+機率命中才觸發,觸發後進入冷卻期 await prisma.contrastTriggerLog.deleteMany({ where: { characterId: GENKI_ID } }); const belowThreshold = await post(`/personality/${GENKI_ID}/contrast-trigger`, { intimacy: 50, emotionTag: "JOY", roll: 0.01, }); if (belowThreshold.triggered) { throw new Error("親密度未達門檻時不應觸發反差行為"); } const firstTrigger = await post(`/personality/${GENKI_ID}/contrast-trigger`, { intimacy: 80, emotionTag: "JOY", roll: 0.01, }); if (!firstTrigger.triggered) { throw new Error("條件皆滿足時應觸發反差行為"); } const duringCooldown = await post(`/personality/${GENKI_ID}/contrast-trigger`, { intimacy: 80, emotionTag: "JOY", roll: 0.01, }); if (duringCooldown.triggered) { throw new Error("冷卻期內同類反差不應再次觸發"); } const tenDaysLater = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toISOString(); const afterCooldown = await post(`/personality/${GENKI_ID}/contrast-trigger`, { intimacy: 80, emotionTag: "JOY", roll: 0.01, now: tenDaysLater, }); if (!afterCooldown.triggered) { throw new Error("冷卻期過後應可再次觸發反差行為"); } // G-6 可愛度行為機制:撒嬌/鬧彆扭節奏/吃醋/專屬揭露的觸發條件 const spoiledOk = await get("/personality/cuteness/spoiled?intimacy=70&emotionTag=JOY"); const spoiledBlocked = await get("/personality/cuteness/spoiled?intimacy=30&emotionTag=JOY"); if (!spoiledOk.canActSpoiled || spoiledBlocked.canActSpoiled) { throw new Error("撒嬌觸發條件(親密度 ≥ 60 且情緒為愉悅/低落)不符預期"); } const grumpyOk = await get("/personality/cuteness/grumpy-recovery?turns=2"); const grumpyForce = await get("/personality/cuteness/grumpy-recovery?turns=3"); if (grumpyOk.shouldForceRecovery || !grumpyForce.shouldForceRecovery) { throw new Error("鬧彆扭節奏控制(超過兩三輪應強制恢復)不符預期"); } const jealousy = await get(`/personality/cuteness/third-party?text=${encodeURIComponent("他人很好,我比較喜歡他")}`); const noJealousy = await get(`/personality/cuteness/third-party?text=${encodeURIComponent("今天天氣好好")}`); if (!jealousy.detected || noJealousy.detected) { throw new Error("第三者好感訊號偵測不符預期"); } const exclusiveOk = await get("/personality/cuteness/exclusive?trust=85"); const exclusiveBlocked = await get("/personality/cuteness/exclusive?trust=50"); if (!exclusiveOk.canUnlock || exclusiveBlocked.canUnlock) { throw new Error("專屬揭露門檻(信任度 ≥ 80)不符預期"); } // G-6 記住小事:使用者瑣事即使情緒強度低也應被固化保留 const triviaSessionId = "smoke-g-trivia-" + Date.now(); await post(`/memory/${GENKI_ID}/sessions/${triviaSessionId}/messages`, { role: "user", content: "我最近迷上了做甜點", emotionTag: "CALM", emotionIntensity: 0.05, }); await post(`/memory/${GENKI_ID}/sessions/${triviaSessionId}/consolidate`, {}); const triviaSaved = await prisma.semanticMemory.findFirst({ where: { characterId: GENKI_ID, fact: "我最近迷上了做甜點" }, }); if (!triviaSaved) { throw new Error("使用者瑣事未被固化保留為語意記憶"); } // 清理本次測試建立的資料,避免重複執行時累積髒資料 await prisma.character.deleteMany({ where: { id: COLD_ID } }); await prisma.semanticMemory.deleteMany({ where: { id: triviaSaved.id } }); await prisma.contrastTriggerLog.deleteMany({ where: { characterId: GENKI_ID } }); await resetEmotion(GENKI_ID, { calm: 70, joy: 20, sad: 0, alert: 0, shy: 0, grumpy: 0 }); }