현재 작업 상태 저장

This commit is contained in:
Yun Chan 2026-06-27 11:20:24 +09:00
parent 07cc67761e
commit 6bd91b0d5e
674 changed files with 8726 additions and 298 deletions

View file

@ -221,9 +221,40 @@ def _is_local_origin(origin: str) -> bool:
return host in {"localhost", "127.0.0.1", "::1"}
def _frontend_origin_map() -> dict[str, str]:
mapped: dict[str, str] = {}
for api_host, frontend_origin in settings.frontend_origin_map.items():
host = (api_host or "").strip().lower().rstrip(".")
origin = _url_origin(frontend_origin)
if host and origin:
mapped[host] = origin
return mapped
def _dev_login_extra_origins() -> set[str]:
return {
origin
for value in settings.auth_dev_login_extra_origins
if (origin := _url_origin(value))
}
def _dev_login_extra_hosts() -> set[str]:
return {
host
for origin in _dev_login_extra_origins()
if (host := (urlsplit(origin).hostname or "").lower())
}
def _is_dev_login_allowed_origin(origin: str) -> bool:
host = (urlsplit(origin).hostname or "").lower()
return _is_local_origin(origin) or origin in _dev_login_extra_origins() or host in _dev_login_extra_hosts()
def _configured_frontend_origins() -> list[str]:
origins: list[str] = []
for value in [settings.frontend_base_url, *settings.cors_origins]:
for value in [settings.frontend_base_url, *settings.frontend_origin_map.values(), *settings.cors_origins]:
origin = _url_origin(value)
if origin and origin not in origins:
origins.append(origin)
@ -243,8 +274,8 @@ def _frontend_origin_for_request(request: Request | None = None) -> str:
forwarded_host = request.headers.get("x-forwarded-host")
host = (forwarded_host or request.headers.get("host") or "").split(",", 1)[0].strip()
hostname = host.rsplit(":", 1)[0].lower() if host else ""
if hostname == "api-vignette.chanpaca.net":
return "https://vignette.chanpaca.net"
if mapped_origin := _frontend_origin_map().get(hostname):
return mapped_origin
if hostname in {"localhost", "127.0.0.1", "::1"}:
return fallback
@ -348,15 +379,24 @@ def _dev_login_available(request: Request) -> bool:
if settings.environment != "dev" or not settings.auth_dev_login_enabled:
return False
if _dev_login_extra_origins():
return True
saw_browser_origin = False
for header_name in ("origin", "referer"):
origin = _url_origin(request.headers.get(header_name))
if origin and not _is_local_origin(origin):
return False
if origin:
saw_browser_origin = True
if not _is_dev_login_allowed_origin(origin):
return False
if not saw_browser_origin and _dev_login_extra_origins():
return True
forwarded_host = request.headers.get("x-forwarded-host")
host = (forwarded_host or request.headers.get("host") or "").split(",", 1)[0].strip()
origin = _url_origin(f"http://{host}") if host else None
return bool(origin and _is_local_origin(origin))
return bool(origin and _is_dev_login_allowed_origin(origin))
@router.get("/config", response_model=AuthConfigResponse)

View file

@ -381,8 +381,14 @@ async def _load_session_or_404(
principal: Principal,
*,
allow_ended: bool = False,
include_turn_evaluation: bool = False,
) -> InProcSession:
sess = await session_persistence.load_session(session_id, principal, allow_ended=True)
sess = await session_persistence.load_session(
session_id,
principal,
allow_ended=True,
include_turn_evaluation=include_turn_evaluation,
)
if sess is not None:
store.put(sess)
elif runtime_fallback_allowed():
@ -874,7 +880,11 @@ async def get_session_detail(
) -> SessionDetailResponse:
"""Return a learner-owned session with transcript for resume/history."""
_ensure_learner(principal)
sess = await _load_session_or_404(session_id, principal, allow_ended=True)
sess = await _load_session_or_404(
session_id,
principal,
allow_ended=True,
)
return _session_detail(sess, review_ready=await _review_ready(sess, principal))
@ -951,7 +961,12 @@ async def get_session_review(
) -> SessionReviewResponse:
"""Return a learner-safe review built only from the stored session transcript."""
_ensure_learner(principal)
sess = await _load_session_or_404(session_id, principal, allow_ended=True)
sess = await _load_session_or_404(
session_id,
principal,
allow_ended=True,
include_turn_evaluation=True,
)
visible_turns = _learner_visible_turns(sess)
hidden_turns = len(visible_turns) != len(sess.turns)

View file

@ -49,11 +49,13 @@ _MAX_AUDIO_BYTES = 10 * 1024 * 1024
async def voice_health() -> JSONResponse:
"""Return voice service readiness."""
available = voice_service.is_available()
tts_provider = voice_service.tts_provider()
body = {
"status": "ok" if available else "degraded",
"available": available,
"stt_model": voice_svc.STT_MODEL,
"tts_model": voice_svc.TTS_MODEL,
"tts_provider": tts_provider,
"reason": None if available else "OPENAI_API_KEY is not configured",
}
return JSONResponse(body, status_code=200 if available else 503)
@ -108,6 +110,7 @@ async def voice_ws(websocket: WebSocket) -> None:
"session_id": session_id,
"voice": voice_preset.openai_voice,
"preset": voice_preset.preset,
"tts_provider": voice_service.tts_provider(),
"state": "idle",
**bind_meta,
},
@ -374,7 +377,12 @@ async def _run_turn_and_speak(
# TTS speaking state comes before chunk metadata and binary audio.
await _safe_send_json(
websocket,
{"type": "state", "state": "speaking", "voice": voice_preset.openai_voice},
{
"type": "state",
"state": "speaking",
"voice": voice_preset.openai_voice,
"tts_provider": voice_service.tts_provider(),
},
)
try:
n = 0