import { prisma } from "@kokorone/db"; const API_PORT = process.env.PORT_API ?? "3001"; const CHARACTER_ID = "seed-character-genki"; const ORIGINAL_EMOTION = { calm: 70, joy: 20, sad: 0, alert: 0, shy: 0, grumpy: 0 }; const BASELINE = { calm: 100, joy: 0, sad: 0, alert: 0, shy: 0, grumpy: 0 }; async function resetEmotion(overrides = BASELINE) { await prisma.emotionState.upsert({ where: { characterId: CHARACTER_ID }, update: { ...overrides, updatedAt: new Date() }, create: { characterId: CHARACTER_ID, ...overrides }, }); } async function postInput(body) { const res = await fetch(`http://localhost:${API_PORT}/emotion/${CHARACTER_ID}/input`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { throw new Error(`POST /emotion/${CHARACTER_ID}/input 回傳 ${res.status}`); } return res.json(); } async function getState(now) { const url = `http://localhost:${API_PORT}/emotion/${CHARACTER_ID}${now ? `?now=${encodeURIComponent(now)}` : ""}`; const res = await fetch(url); if (!res.ok) { throw new Error(`GET /emotion/${CHARACTER_ID} 回傳 ${res.status}`); } return res.json(); } function dominantOf(state) { const candidates = [ ["JOY", state.joy], ["SAD", state.sad], ["ALERT", state.alert], ["SHY", state.shy], ["GRUMPY", state.grumpy], ]; const [tag, value] = candidates.reduce((best, cur) => (cur[1] > best[1] ? cur : best), ["CALM", 5]); return value > 5 ? tag : "CALM"; } function assertDominant(state, expected, label) { const actual = dominantOf(state); if (actual !== expected) { throw new Error(`${label}:預期主導情緒為 ${expected},實際為 ${actual}(${JSON.stringify(state)})`); } } export default async function smokeD() { // D-1 情緒標記器:正向/負向/衝突語氣輸入分別得到對應標記 await resetEmotion(); const positive = await postInput({ text: "謝謝你,我好開心!" }); assertDominant(positive.state, "JOY", "正向輸入"); await resetEmotion(); const negative = await postInput({ text: "今天過得好難過,很失望" }); assertDominant(negative.state, "SAD", "負向輸入"); await resetEmotion(); const conflict = await postInput({ text: "你這個笨蛋,滾開!" }); assertDominant(conflict.state, "ALERT", "衝突語氣輸入"); if (conflict.style.sentenceLength !== "short" || conflict.style.initiative !== "low") { throw new Error("D-3 警戒狀態的回應風格參數不符預期"); } // D-4 情緒狀態機:連續負向(衝突持續)輸入 → 警戒繼續惡化為低落 const stillConflicted = await postInput({ text: "我還是很生氣", conflictContinues: true }); assertDominant(stillConflicted.state, "SAD", "警戒 --> 低落(衝突持續)"); // 警戒 --> 平靜(確認無威脅) await resetEmotion({ calm: 0, joy: 0, sad: 0, alert: 100, shy: 0, grumpy: 0 }); const safeAgain = await postInput({ text: "沒事了", threatConfirmedSafe: true }); assertDominant(safeAgain.state, "CALM", "警戒 --> 平靜(確認無威脅)"); // 低落 --> 平靜(被安慰) await resetEmotion({ calm: 50, joy: 0, sad: 50, alert: 0, shy: 0, grumpy: 0 }); const comforted = await postInput({ text: "謝謝你安慰我", comforted: true }); assertDominant(comforted.state, "CALM", "低落 --> 平靜(被安慰)"); // 平靜 --> 害羞(被稱讚) await resetEmotion(); const praised = await postInput({ text: "你今天好可愛喔" }); assertDominant(praised.state, "SHY", "平靜 --> 害羞(被稱讚)"); // 平靜 --> 彆扭(小型負向事件)→ 被哄後快速恢復平靜 await resetEmotion(); const grumpy = await postInput({ text: "你都不理我" }); assertDominant(grumpy.state, "GRUMPY", "平靜 --> 彆扭(小型負向事件)"); const placated = await postInput({ text: "好啦好啦", placated: true }); assertDominant(placated.state, "CALM", "彆扭 --> 平靜(被哄)"); // D-3 情緒→回應風格輸出:愉悅指向多話/高主動,低落指向簡短/低主動 await resetEmotion(); const joyful = await postInput({ text: "謝謝你,我好開心!" }); if (joyful.style.sentenceLength !== "long" || joyful.style.initiative !== "high") { throw new Error("D-3 愉悅狀態的回應風格參數不符預期"); } await resetEmotion(); const sadResult = await postInput({ text: "今天過得好難過,很失望" }); if (sadResult.style.sentenceLength !== "short" || sadResult.style.initiative !== "low") { throw new Error("D-3 低落狀態的回應風格參數不符預期"); } // D-2 時間衰減:模擬經過數小時後情緒值回落;跨 session(重新讀取)仍可讀到殘留 await resetEmotion({ calm: 0, joy: 100, sad: 0, alert: 0, shy: 0, grumpy: 0 }); const immediate = await getState(); if (immediate.joy < 90) { throw new Error("尚未經過時間就已衰減,時間衰減函數計算有誤"); } const threeHoursLater = new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(); const decayed = await getState(threeHoursLater); if (decayed.joy >= immediate.joy) { throw new Error("模擬時間推進後情緒值未回落"); } if (decayed.joy <= 0) { throw new Error("時間推進後情緒殘留應仍大於 0(尚未完全衰減)"); } // 跨 session:不重啟服務也不清狀態,重新 GET 一次應讀到同一份殘留(持久化於資料庫,而非行程記憶體) const reread = await getState(threeHoursLater); if (Math.abs(reread.joy - decayed.joy) > 0.01) { throw new Error("重新讀取時殘留情緒值不一致,情緒狀態未正確持久化"); } const twentyHoursLater = new Date(Date.now() + 20 * 60 * 60 * 1000).toISOString(); const fullyDecayed = await getState(twentyHoursLater); if (dominantOf(fullyDecayed) !== "CALM") { throw new Error("長時間衰減後應回歸平靜"); } // 還原種子角色原始展示用情緒值,避免影響其他群組或人工檢視 await resetEmotion(ORIGINAL_EMOTION); }