mmday-firebase/src/handlers/debugHandlers.ts
윤정민 40dcb41d99 Add debug endpoints and scripts to evaluate chat tools and models.
특정 uid로 실제 도구·모델 루프를 돌려 호출된 도구·토큰·속도·캐시 히트를
보고하는 /debug/chatToolsSheet, /debug/chatProbe(Sheet) 엔드포인트를 추가한다
(요청별 model 오버라이드로 A/B 가능). prod에 같은 검증을 돌리는
scripts/chat-tools-sheet.ts도 추가한다.

무인증 + 임의 uid의 개인 데이터를 노출하는 검수용 임시 엔드포인트다.
2026-06-16 16:52:21 +09:00

133 lines
5.8 KiB
TypeScript

import { onRequest } from "firebase-functions/https";
import { logger } from "firebase-functions";
import { runDailyArchive } from "../scheduled/dailyArchive";
import { forceSyncDay } from "../services/gameSyncService";
import { runChatToolsSheet, renderSheetMarkdown } from "../services/chatToolsSheetService";
import { runChatProbe, runChatProbeBatch, renderProbeMarkdown } from "../services/chatProbeService";
import { sendError } from "../middleware/errors";
import { daysAgoKst, parseDateString } from "../types/dateString";
/**
* 임시 디버그 핸들러. 인증 없음 — 운영 안정화 후 제거할 것.
*
* - GET/POST `/debug/forceSync` — 어제(KST) 기준으로 라이브 데이터 sync 실행.
* - GET/POST `/debug/forceSync?date=YYYY-MM-DD` — 지정한 날짜를 강제 sync.
* - GET/POST `/debug/dailyArchive` — 어제(KST) 기준으로 dailyArchive 본체 실행.
* - GET/POST `/debug/dailyArchive?date=YYYY-MM-DD` — 지정한 날짜를 archive 대상으로 실행.
* - GET `/debug/chatToolsSheet?uid=UID` — 그 유저 기준으로 짹 도구 전체를 실호출한
* 결과 시트(JSON)를 반환. `&format=md`면 Markdown, `&player=`·`&date=`로 케이스 추가.
* - GET `/debug/chatProbe?uid=UID&q=문장` — 자연어 문장 1개를 실제 모델+도구 루프로
* 돌리고, 모델이 고른 도구·인자(toolCalls)와 최종 응답을 반환. `&model=`로 테스트 모델 지정.
* - GET `/debug/chatProbeSheet?uid=UID` — 기본 자연어 문장 세트를 배치로 프로브한
* 결과 시트(토큰·속도·캐시 포함). `&format=md`면 Markdown, `&model=<모델ID>`로 A/B
* (미지정 시 기본 lite. 타임아웃은 모든 모델 공평하게 24초).
* ⚠️ 무인증 + 임의 uid의 개인 데이터(출석·예측)를 노출하고 실제 Vertex 토큰을
* 소모한다 — 검수용 임시 엔드포인트다.
*/
export const debug = onRequest({ timeoutSeconds: 300 }, async (req, res) => {
const segs = req.path.replace(/^\/+|\/+$/g, "").split("/");
const tail = segs.slice(-1)[0];
try {
if (tail === "forceSync") {
const dateParam =
(typeof req.query.date === "string" && req.query.date) ||
(typeof req.body?.date === "string" && req.body.date) ||
undefined;
const dateString = dateParam ?
parseDateString(dateParam) :
daysAgoKst(1);
const ymd = dateString.replace(/-/g, "");
logger.info(`debug.forceSync triggered manually (ymd=${ymd})`);
const result = await forceSyncDay(ymd);
res.status(200).json({ ymd, ...result });
return;
}
if (tail === "dailyArchive") {
const dateParam =
(typeof req.query.date === "string" && req.query.date) ||
(typeof req.body?.date === "string" && req.body.date) ||
undefined;
const overrideDate = dateParam ? parseDateString(dateParam) : undefined;
logger.info(
`debug.dailyArchive triggered manually (date=${overrideDate ?? "yesterday"})`
);
const result = await runDailyArchive(overrideDate);
res.status(200).json(result);
return;
}
if (tail === "chatToolsSheet" || tail === "chat-tools-sheet") {
const uid =
(typeof req.query.uid === "string" && req.query.uid) ||
(typeof req.body?.uid === "string" && req.body.uid) ||
"";
if (!uid) {
res.status(400).json({ error: "uid is required (e.g. ?uid=<UID>)" });
return;
}
const player = typeof req.query.player === "string" ? req.query.player : undefined;
const date = typeof req.query.date === "string" ? req.query.date : undefined;
const format = typeof req.query.format === "string" ? req.query.format : "json";
logger.info(`debug.chatToolsSheet triggered (uid=${uid})`);
const sheet = await runChatToolsSheet(uid, { player, date });
if (format === "md" || format === "markdown") {
res.status(200).type("text/markdown; charset=utf-8").send(renderSheetMarkdown(sheet));
return;
}
res.status(200).json(sheet);
return;
}
if (tail === "chatProbe") {
const uid =
(typeof req.query.uid === "string" && req.query.uid) ||
(typeof req.body?.uid === "string" && req.body.uid) ||
"";
const q =
(typeof req.query.q === "string" && req.query.q) ||
(typeof req.body?.q === "string" && req.body.q) ||
"";
if (!uid || !q) {
res.status(400).json({ error: "uid and q are required (e.g. ?uid=<UID>&q=<문장>)" });
return;
}
const model = typeof req.query.model === "string" ? req.query.model : undefined;
logger.info(`debug.chatProbe triggered (uid=${uid}, model=${model ?? "default"})`);
const result = await runChatProbe(uid, q, model);
res.status(200).json(result);
return;
}
if (tail === "chatProbeSheet") {
const uid =
(typeof req.query.uid === "string" && req.query.uid) ||
(typeof req.body?.uid === "string" && req.body.uid) ||
"";
if (!uid) {
res.status(400).json({ error: "uid is required (e.g. ?uid=<UID>)" });
return;
}
const format = typeof req.query.format === "string" ? req.query.format : "json";
const model = typeof req.query.model === "string" ? req.query.model : undefined;
logger.info(`debug.chatProbeSheet triggered (uid=${uid}, model=${model ?? "default"})`);
const sheet = await runChatProbeBatch(uid, undefined, model);
if (format === "md" || format === "markdown") {
res.status(200).type("text/markdown; charset=utf-8").send(renderProbeMarkdown(sheet));
return;
}
res.status(200).json(sheet);
return;
}
res.status(404).json({ error: `Unknown: ${req.method} ${req.path}` });
} catch (err) {
sendError(res, err);
}
});