Files
Kokorone/scripts/smoke/G.mjs
Jeffery 503024c92f feat: 完成 G 群組 — 角色人格層
- archetype-params.ts:六原型(傲嬌/冷淡/天然呆/元氣/大小姐/三無)參數表,
  串接進 D 群組 EmotionService(情緒觸發閾值)與 E 群組 SentimentLedgerService(信任成長速度)
- 修正一個真實落差:觸發閾值原本只縮放強度、不影響是否觸發,加入 MIN_TRIGGER_INTENSITY
  門檻後「難觸發」原型才會對弱刺激真的沒反應
- expressiveness.ts:G-3 外顯函數,傲嬌反向表達(傲嬌值=內外差值,隨內部親密度先升後降)
- language-style.ts:G-2 稱呼系統(陌生→您、認識→名字、摯友→暱稱,含跨階事件)與角色禁則清單,
  已串接進 F-3 輸出過濾層與口癖後處理
- contrast-behavior.service.ts:G-4 反差萌,親密度+情緒+機率門檻皆滿足才觸發,
  新增 ContrastTriggerLog 資料表記錄冷卻期
- character-creation.service.ts:G-5 由角色設定表一次產生語意記憶/情節記憶/情緒參數/關係初始值
- cuteness-behavior.ts:G-6 撒嬌/鬧彆扭節奏控制/吃醋/笨拙的努力/專屬揭露觸發條件;
  記住小事擴充了 C-4 睡眠固化,使用者瑣事即使情緒強度低也會寫入語意記憶
- scripts/smoke/G.mjs:涵蓋六原型差異化反應、稱呼進化、傲嬌外顯曲線、反差冷卻、
  可愛行為觸發條件、瑣事固化

npm run restart && npm run smoke -- G 皆通過(G-V),A/B/C/D/E/F 群組冒煙測試無回歸。
2026-08-13 11:26:54 +08:00

199 lines
8.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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&current=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 });
}