feat: 完成 H 群組 — 網頁對話介面(第一個可實際使用的垂直切片)
- POST /chat/:characterId:正式對外對話端點,DialogueService 補上情緒標記與親密度 累積(每輪對話走完整管線:標記情緒→檢索記憶→組裝→生成→過濾) - POST /session/:sessionId/end:觸發睡眠固化 - NestJS 啟用 CORS(WEB_ORIGIN,預設 http://localhost:3100),本清單第一次有瀏覽器直接呼叫 api - apps/web/app/chat:對話頁面(ChatView 用戶端元件),電腦雙欄/行動單欄同一份 RWD 程式, 立繪半身像置頂佔 40% 為行動版背景層 - 情緒晶片+親密度數字顯示,跟著每輪回應即時更新 - 動作描寫標記(*…*)以暮空紫小字獨立呈現,與台詞區分 - HeartbeatWave 元件:載入時由左至右畫出、親密度決定振幅、通知紅點心跳脈動, 皆尊重 prefers-reduced-motion;首頁與對話頁共用同一元件 - 色票換成 wiki 主視覺提案頁的完整 token 組(含官方深色模式數值) - scripts/smoke/H.mjs:端到端驗證對話 API 狀態變化、session 結束固化、頁面結構標記; 另以 Playwright + headless Chromium 實際開瀏覽器驗證桌面/行動版面、送出訊息、 動作標記渲染,過程無 console 錯誤 npm run restart && npm run smoke -- H 皆通過(H-V),A~G 群組冒煙測試無回歸。
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { prisma } from "@kokorone/db";
|
||||
|
||||
const API_PORT = process.env.PORT_API ?? "3001";
|
||||
const WEB_PORT = process.env.PORT_WEB ?? "3100";
|
||||
const CHARACTER_ID = "seed-character-genki";
|
||||
const USER_ID = "seed-user-primary";
|
||||
|
||||
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}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export default async function smokeH() {
|
||||
const sessionId = "smoke-h-" + Date.now();
|
||||
|
||||
await prisma.emotionState.update({
|
||||
where: { characterId: CHARACTER_ID },
|
||||
data: { calm: 100, joy: 0, sad: 0, alert: 0, shy: 0, grumpy: 0, updatedAt: new Date() },
|
||||
});
|
||||
const before = await get(`/relationships/${CHARACTER_ID}/${USER_ID}`);
|
||||
const intimacyBefore = before.relationship.intimacy;
|
||||
|
||||
// H-1 對話 API:一輪完整對話回傳回應、情緒狀態與親密度變化
|
||||
const result = await post(`/chat/${CHARACTER_ID}`, {
|
||||
userId: USER_ID,
|
||||
sessionId,
|
||||
text: "謝謝你,我今天好開心!",
|
||||
});
|
||||
if (!result.text || typeof result.text !== "string") {
|
||||
throw new Error("對話 API 未回傳文字內容");
|
||||
}
|
||||
if (result.context.emotion.dominant !== "JOY") {
|
||||
throw new Error(`對話後情緒狀態應變為愉悅,實際為 ${result.context.emotion.dominant}`);
|
||||
}
|
||||
if (result.context.relationship.intimacy <= intimacyBefore) {
|
||||
throw new Error("對話後親密度應有變化(累積日常互動)");
|
||||
}
|
||||
|
||||
// H-1 session 生命週期:POST /session/:id/end 觸發睡眠固化,工作記憶應被清空
|
||||
const contextBefore = await get(`/memory/${CHARACTER_ID}/sessions/${sessionId}/messages`);
|
||||
if (contextBefore.context.length === 0) {
|
||||
throw new Error("對話後工作記憶應保留本輪內容,session 結束前不應為空");
|
||||
}
|
||||
await post(`/session/${sessionId}/end`, { characterId: CHARACTER_ID });
|
||||
const contextAfter = await get(`/memory/${CHARACTER_ID}/sessions/${sessionId}/messages`);
|
||||
if (contextAfter.context.length !== 0) {
|
||||
throw new Error("session 結束後應觸發睡眠固化並清空工作記憶");
|
||||
}
|
||||
|
||||
// H-2~H-5:對話頁面應包含情緒晶片、親密度顯示、心跳波形、立繪佔位區塊的結構標記
|
||||
const pageRes = await fetch(`http://localhost:${WEB_PORT}/chat`);
|
||||
if (!pageRes.ok) {
|
||||
throw new Error(`GET /chat 回傳 ${pageRes.status}`);
|
||||
}
|
||||
const html = await pageRes.text();
|
||||
if (!html.includes("親密度")) {
|
||||
throw new Error("對話頁面缺少親密度顯示(H-2)");
|
||||
}
|
||||
if (!html.includes("heartbeat-wave-line")) {
|
||||
throw new Error("對話頁面缺少心跳波形母題(H-4)");
|
||||
}
|
||||
if (!html.includes("立繪")) {
|
||||
throw new Error("對話頁面缺少立繪佔位區塊(H-5)");
|
||||
}
|
||||
|
||||
// 清理本次測試造成的狀態變化,避免影響其他群組或人工檢視
|
||||
await prisma.relationship.update({
|
||||
where: { characterId_userId: { characterId: CHARACTER_ID, userId: USER_ID } },
|
||||
data: { intimacy: 25 },
|
||||
});
|
||||
await prisma.emotionState.update({
|
||||
where: { characterId: CHARACTER_ID },
|
||||
data: { calm: 70, joy: 20, sad: 0, alert: 0, shy: 0, grumpy: 0, updatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user