대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정

SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리

페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침

버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)

검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
This commit is contained in:
Yun Chan 2026-06-27 02:30:46 +09:00
parent cb2aebd76c
commit 085460b5e0
327 changed files with 31226 additions and 1829 deletions

View file

@ -0,0 +1,101 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { expect, test } from "@playwright/test";
const PERSONA_CODES = ["p4", "p5", "p6", "p7"] as const;
test.describe("first-party Live2D assets", () => {
test("serves model3 manifests and exp3 expressions for every persona", async ({ request }) => {
const indexResponse = await request.get("/live2d/personas/index.json");
expect(indexResponse.ok(), await indexResponse.text()).toBeTruthy();
const index = (await indexResponse.json()) as {
personas: { code: string; model3: string; expressionCount: number }[];
};
expect(index.personas).toHaveLength(4);
for (const code of PERSONA_CODES) {
const modelResponse = await request.get(`/live2d/personas/${code}/${code}.model3.json`);
expect(modelResponse.ok(), await modelResponse.text()).toBeTruthy();
const model = (await modelResponse.json()) as {
Version: number;
Vignette: { ModelId: string; PersonaCode: string };
FileReferences: { Expressions: { Name: string; File: string }[] };
};
expect(model.Version).toBe(3);
expect(model.Vignette.ModelId).toBe(`vignette-${code}-live2d`);
expect(model.Vignette.PersonaCode.toLowerCase()).toBe(code);
expect(model.FileReferences.Expressions.length).toBeGreaterThanOrEqual(20);
expect(model.FileReferences.Expressions.map((expression) => expression.Name)).toEqual(
expect.arrayContaining(["joy", "sad", "angry", "rage"]),
);
const rage = model.FileReferences.Expressions.find((expression) => expression.Name === "rage");
expect(rage, `${code} rage expression`).toBeTruthy();
const expressionResponse = await request.get(`/live2d/personas/${code}/${rage!.File}`);
expect(expressionResponse.ok(), await expressionResponse.text()).toBeTruthy();
const expression = (await expressionResponse.json()) as {
Type: string;
Version: number;
FadeInTime: number;
FadeOutTime: number;
Parameters: { Id: string; Value: number; Blend: string }[];
};
expect(expression.Type).toBe("Live2D Expression");
expect(expression.Version).toBe(3);
expect(expression.FadeInTime).toBe(0.26);
expect(expression.FadeOutTime).toBe(0.32);
expect(expression.Parameters.length).toBeGreaterThanOrEqual(12);
expect(expression.Parameters.map((parameter) => parameter.Id)).toEqual(
expect.arrayContaining(["ParamMouthOpenY", "ParamMouthForm", "ParamEyeLOpen"]),
);
}
});
test("keeps legacy demo paths blocked while allowing first-party persona assets at the Pages function", async () => {
const functionPath = path.resolve("functions", "live2d", "[[path]].js");
const { onRequest } = (await import(pathToFileURL(functionPath).href)) as {
onRequest: (context: {
request: Request;
params: { path: string[] };
env: { ASSETS: { fetch: (request: Request) => Promise<Response> } };
}) => Promise<Response>;
};
const allowed = await onRequest({
request: new Request("https://vignette.example/live2d/personas/p4/p4.model3.json"),
params: { path: ["personas", "p4", "p4.model3.json"] },
env: {
ASSETS: {
fetch: async () => new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
},
},
});
expect(allowed.status).toBe(200);
expect(allowed.headers.get("x-robots-tag")).toBe("noindex");
const legacy = await onRequest({
request: new Request("https://vignette.example/live2d/mao/Mao.model3.json"),
params: { path: ["mao", "Mao.model3.json"] },
env: {
ASSETS: {
fetch: async () => new Response("should not be called", { status: 200 }),
},
},
});
expect(legacy.status).toBe(404);
const traversal = await onRequest({
request: new Request("https://vignette.example/live2d/personas/p4/../mao/Mao.model3.json"),
params: { path: ["personas", "p4", "..", "mao", "Mao.model3.json"] },
env: {
ASSETS: {
fetch: async () => new Response("should not be called", { status: 200 }),
},
},
});
expect(traversal.status).toBe(404);
});
});