项目文件夹

文件
wehub-resource-sync 2c632336aa
CI / Viewer CI (push) Successful in 13m37s
CI / Core CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:38 +08:00

387 行
16 KiB
Python

from __future__ import annotations
import asyncio
import logging
from pathlib import Path
from typing import Any, Protocol
from fastapi import HTTPException
from agent.prompts import (
DESIGNER_PROMPT_NAME,
GEMINI_DESIGNER_PROMPT_NAME,
OPENAI_DESIGNER_PROMPT_NAME,
)
from storage.identifiers import validate_record_id
from storage.repo import StorageRepo
from storage.revisions import (
active_cost_path,
active_inputs_dir,
active_model_path,
active_prompt_path,
active_provenance_path,
active_revision_id,
active_traces_dir,
validate_revision_id,
)
from storage.trajectories import (
COMPRESSED_TRAJECTORY_FILENAME,
SYSTEM_PROMPT_FILENAMES,
TRAJECTORY_FILENAME,
unroll_record_trajectory,
)
from viewer.api.media import trace_media_type
from viewer.api.store_types import MaterializeRecordAssetsResult
logger = logging.getLogger(__name__)
SYSTEM_PROMPT_REQUEST_NAMES = SYSTEM_PROMPT_FILENAMES | {
DESIGNER_PROMPT_NAME,
OPENAI_DESIGNER_PROMPT_NAME,
GEMINI_DESIGNER_PROMPT_NAME,
}
class MaterializingViewerStore(Protocol):
repo: StorageRepo
def materialize_record_assets(
self,
record_id: str,
*,
force: bool = False,
target: str = "full",
) -> MaterializeRecordAssetsResult: ...
class ViewerFileResolver:
def __init__(self, store: MaterializingViewerStore) -> None:
self.store = store
self.repo = store.repo
def resolve_record_folder(self, record_id: str) -> Path:
self._validate_record_id(record_id)
record_dir = self.repo.layout.record_dir(record_id)
if not record_dir.exists() or not record_dir.is_dir():
raise HTTPException(status_code=404, detail=f"Record folder not found: {record_id}")
return record_dir
def resolve_record_target(self, record_id: str, file_path: str) -> tuple[Path, Path]:
self._validate_record_id(record_id)
record_dir = self.repo.layout.record_dir(record_id)
if not record_dir.exists():
raise HTTPException(status_code=404, detail=f"Record not found: {record_id}")
requested_path = self._validated_relative_path(file_path)
if requested_path.parts == ("model.urdf",):
root = self.repo.layout.record_materialization_dir(record_id).resolve()
target = self.repo.layout.record_materialization_urdf_path(record_id).resolve()
elif requested_path.parts == ("compile_report.json",):
root = self.repo.layout.record_materialization_dir(record_id).resolve()
target = self.repo.layout.record_materialization_compile_report_path(
record_id
).resolve()
elif len(requested_path.parts) >= 2 and requested_path.parts[0] == "assets":
root = self.repo.layout.record_materialization_dir(record_id).resolve()
target = (root / requested_path).resolve()
elif requested_path.parts in {
("prompt.txt",),
("model.py",),
("provenance.json",),
("cost.json",),
}:
record = self.repo.read_json(self.repo.layout.record_metadata_path(record_id))
if not isinstance(record, dict):
raise HTTPException(status_code=404, detail=f"Record not found: {record_id}")
root = self.repo.layout.record_revision_dir(
record_id,
active_revision_id(self.repo, record_id, record=record),
).resolve()
active_paths = {
("prompt.txt",): active_prompt_path(self.repo, record_id, record=record),
("model.py",): active_model_path(self.repo, record_id, record=record),
("provenance.json",): active_provenance_path(self.repo, record_id, record=record),
("cost.json",): active_cost_path(self.repo, record_id, record=record),
}
target = active_paths[requested_path.parts].resolve()
root = target.parent.resolve()
elif len(requested_path.parts) >= 2 and requested_path.parts[0] == "inputs":
record = self.repo.read_json(self.repo.layout.record_metadata_path(record_id))
if not isinstance(record, dict):
raise HTTPException(status_code=404, detail=f"Record not found: {record_id}")
root = active_inputs_dir(self.repo, record_id, record=record).resolve()
target = (root / Path(*requested_path.parts[1:])).resolve()
else:
root = record_dir.resolve()
target = (record_dir / requested_path).resolve()
self._ensure_within_root(target, root)
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
return root, target
async def resolve_record_target_with_materialization(
self, record_id: str, file_path: str
) -> tuple[Path, Path]:
requested_path = Path(file_path)
try:
return self.resolve_record_target(record_id, file_path)
except HTTPException as exc:
if exc.status_code != 404 or not should_attempt_materialize_for_record_path(file_path):
raise
try:
await asyncio.to_thread(
self.store.materialize_record_assets,
record_id,
target="visual",
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except RuntimeError as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
try:
return self.resolve_record_target(record_id, file_path)
except HTTPException as exc:
needs_forced_rebuild = exc.status_code == 404 and (
len(requested_path.parts) >= 2
and requested_path.parts[0] == "assets"
and requested_path.parts[1] in {"meshes", "glb", "viewer"}
)
if not needs_forced_rebuild:
raise
try:
await asyncio.to_thread(
self.store.materialize_record_assets,
record_id,
force=True,
target="visual",
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except RuntimeError as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return self.resolve_record_target(record_id, file_path)
def resolve_record_trace_target(self, record_id: str, file_path: str) -> tuple[Path, str]:
self._validate_record_id(record_id)
record_dir = self.repo.layout.record_dir(record_id)
record = self.repo.read_json(self.repo.layout.record_metadata_path(record_id))
if not record_dir.exists() or not isinstance(record, dict):
raise HTTPException(status_code=404, detail=f"Record not found: {record_id}")
if self._record_trace_unavailable(record):
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
provenance = self.repo.read_json(
active_provenance_path(self.repo, record_id, record=record)
)
requested_path = self._validated_relative_path(file_path)
if len(requested_path.parts) != 1:
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
requested_name = requested_path.name
revision_id = active_revision_id(self.repo, record_id, record=record)
trace_root = active_traces_dir(self.repo, record_id, record=record).resolve()
if requested_name == TRAJECTORY_FILENAME:
try:
target = unroll_record_trajectory(
self.repo,
record_id,
revision_id=revision_id if record.get("schema_version") == 3 else None,
)
except FileNotFoundError as exc:
raise HTTPException(
status_code=404, detail=f"Trace file not found: {file_path}"
) from exc
return target, "application/x-ndjson"
if requested_name == COMPRESSED_TRAJECTORY_FILENAME:
target = (trace_root / requested_name).resolve()
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
return target, "application/zstd"
prompting = provenance.get("prompting") if isinstance(provenance, dict) else None
prompt_sha = (
str(prompting.get("system_prompt_sha256"))
if isinstance(prompting, dict) and prompting.get("system_prompt_sha256")
else None
)
prompt_name = (
str(prompting.get("system_prompt_file"))
if isinstance(prompting, dict) and prompting.get("system_prompt_file")
else None
)
if requested_name in SYSTEM_PROMPT_REQUEST_NAMES or (
prompt_name is not None and requested_name == prompt_name
):
if prompt_sha:
target = self.repo.layout.system_prompt_path(prompt_sha)
if target.exists() and target.is_file():
return target, "text/plain"
logger.warning(
"Canonical system prompt missing for record_id=%s sha=%s requested_name=%s prompt_name=%s; refusing trace-local fallback",
record_id,
prompt_sha,
requested_name,
prompt_name,
)
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
fallback_name = prompt_name or requested_name
target = (trace_root / Path(fallback_name).name).resolve()
if target.exists() and target.is_file():
logger.warning(
"Serving trace-local system prompt fallback for record_id=%s requested_name=%s prompt_name=%s",
record_id,
requested_name,
prompt_name,
)
return target, "text/plain"
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
target = (trace_root / requested_path).resolve()
self._ensure_within_root(target, trace_root)
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
return target, trace_media_type(target)
def resolve_record_revision_target(
self,
record_id: str,
revision_id: str,
file_path: str,
) -> tuple[Path, Path]:
self._validate_record_id(record_id)
revision_id = validate_revision_id(revision_id)
root = self.repo.layout.record_revision_dir(record_id, revision_id).resolve()
if not root.exists() or not root.is_dir():
raise HTTPException(status_code=404, detail=f"Revision not found: {revision_id}")
requested_path = self._validated_relative_path(file_path)
target = (root / requested_path).resolve()
self._ensure_within_root(target, root)
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
return root, target
def resolve_record_revision_trace_target(
self,
record_id: str,
revision_id: str,
file_path: str,
) -> tuple[Path, str]:
self._validate_record_id(record_id)
record = self.repo.read_json(self.repo.layout.record_metadata_path(record_id))
if not isinstance(record, dict):
raise HTTPException(status_code=404, detail=f"Record not found: {record_id}")
if self._record_trace_unavailable(record):
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
requested_path = self._validated_relative_path(file_path)
if len(requested_path.parts) != 1:
raise HTTPException(status_code=400, detail="Invalid file path")
revision_id = validate_revision_id(revision_id)
requested_name = requested_path.name
trace_root = self.repo.layout.record_revision_traces_dir(record_id, revision_id).resolve()
if requested_name == TRAJECTORY_FILENAME:
try:
target = unroll_record_trajectory(
self.repo,
record_id,
revision_id=revision_id,
)
except FileNotFoundError as exc:
raise HTTPException(
status_code=404, detail=f"Trace file not found: {file_path}"
) from exc
return target, "application/x-ndjson"
target = (trace_root / requested_name).resolve()
self._ensure_within_root(target, trace_root)
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail=f"Trace file not found: {file_path}")
return target, trace_media_type(target)
def _record_trace_unavailable(self, record: dict[str, Any]) -> bool:
creator = record.get("creator")
return isinstance(creator, dict) and creator.get("trace_available") is False
def resolve_staging_root(self, run_id: str, record_id: str) -> Path:
self._validate_run_id(run_id)
self._validate_record_id(record_id)
staging_dir = self.repo.layout.run_staging_dir(run_id) / record_id
if not staging_dir.exists() or not staging_dir.is_dir():
raise HTTPException(
status_code=404,
detail=f"Staging entry not found: run_id={run_id} record_id={record_id}",
)
return staging_dir.resolve()
def resolve_staging_target(
self, run_id: str, record_id: str, file_path: str
) -> tuple[Path, Path]:
staging_dir = self.resolve_staging_root(run_id, record_id)
requested_path = self._validated_relative_path(file_path)
target = (staging_dir / requested_path).resolve()
self._ensure_within_root(target, staging_dir)
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
return staging_dir, target
def resolve_staging_trace_target(
self,
run_id: str,
record_id: str,
file_path: str,
) -> tuple[Path, str]:
requested_path = self._validated_relative_path(file_path)
if len(requested_path.parts) != 1:
raise HTTPException(status_code=400, detail="Invalid file path")
requested_name = requested_path.name
_, target = self.resolve_staging_target(run_id, record_id, f"traces/{requested_name}")
return target, trace_media_type(target)
def _validate_record_id(self, record_id: str) -> None:
try:
validate_record_id(record_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid record ID format")
@staticmethod
def _validate_run_id(run_id: str) -> None:
if not run_id.startswith("run_"):
raise HTTPException(status_code=400, detail="Invalid run ID format")
@staticmethod
def _validated_relative_path(file_path: str) -> Path:
requested_path = Path(file_path)
if requested_path.is_absolute() or ".." in requested_path.parts:
raise HTTPException(status_code=400, detail="Invalid file path")
return requested_path
@staticmethod
def _ensure_within_root(target: Path, root: Path) -> None:
try:
target.relative_to(root)
except ValueError as exc:
raise HTTPException(status_code=403, detail="Access denied") from exc
def should_attempt_materialize_for_record_path(file_path: str) -> bool:
requested_path = Path(file_path)
if requested_path.parts == ("model.urdf",):
return True
if len(requested_path.parts) >= 2 and requested_path.parts[0] == "assets":
return requested_path.parts[1] in {"meshes", "glb", "viewer"}
return False