rebase
This commit is contained in:
50
.dockerignore
Normal file
50
.dockerignore
Normal file
@@ -0,0 +1,50 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/.pytest_cache/
|
||||
**/.ruff_cache/
|
||||
**/.mypy_cache/
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
|
||||
# Node / Frontend
|
||||
**/node_modules/
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
npm-debug.log*
|
||||
yarn-*
|
||||
|
||||
# Environment & secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Data & uploads (kept on volumes, not in image)
|
||||
data/
|
||||
!data/.gitkeep
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# IDE / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Tests artifacts
|
||||
.pytest_cache/
|
||||
tests/.cache/
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
40
.env.example
40
.env.example
@@ -1,38 +1,62 @@
|
||||
# ============================================================
|
||||
# AI-RPG environment configuration
|
||||
# Copy to .env and edit before running `docker compose up`.
|
||||
# ============================================================
|
||||
|
||||
# === Database ===
|
||||
POSTGRES_USER=airpg
|
||||
POSTGRES_PASSWORD=airpg
|
||||
POSTGRES_DB=airpg
|
||||
DATABASE_URL=postgresql+asyncpg://airpg:airpg@db:5432/airpg
|
||||
DATABASE_URL_SYNC=postgresql+psycopg2://airpg:airpg@db:5432/airpg
|
||||
# Used by backend when running OUTSIDE docker (local dev). Inside docker-compose
|
||||
# this is overridden in docker-compose.yml to point at the `db` service.
|
||||
DATABASE_URL=postgresql+asyncpg://airpg:airpg@localhost:5432/airpg
|
||||
DATABASE_URL_SYNC=postgresql+psycopg2://airpg:airpg@localhost:5432/airpg
|
||||
|
||||
# === Qdrant ===
|
||||
QDRANT_URL=http://qdrant:6333
|
||||
QDRANT_URL=http://localhost:6333
|
||||
QDRANT_API_KEY=
|
||||
|
||||
# === LLM ===
|
||||
# === LLM (OpenAI-compatible) ===
|
||||
# Examples:
|
||||
# OpenAI: https://api.openai.com/v1
|
||||
# Ollama (host): http://host.docker.internal:11434/v1 ← for docker
|
||||
# Ollama (local): http://localhost:11434/v1 ← for non-docker dev
|
||||
# vLLM: http://localhost:8000/v1
|
||||
# Leave empty to use MockLlmClient (replay-based, for dev/tests).
|
||||
LLM_API_URL=http://host.docker.internal:11434/v1
|
||||
LLM_API_KEY=
|
||||
LLM_MODEL=qwen2.5-7b-instruct
|
||||
LLM_TIMEOUT_SECONDS=60
|
||||
|
||||
# === Embeddings ===
|
||||
# Provider: "offline_hash" (deterministic, dev-only, no HTTP) or "openai"
|
||||
EMBEDDINGS_PROVIDER=offline_hash
|
||||
# If provider=openai and these are empty, falls back to LLM_API_URL / LLM_API_KEY
|
||||
EMBEDDINGS_API_URL=
|
||||
EMBEDDINGS_API_KEY=
|
||||
EMBEDDINGS_MODEL=text-embedding-3-small
|
||||
# For offline_hash: 256. For openai text-embedding-3-small: 1536.
|
||||
# Use admin "Probe dimension" button to auto-detect.
|
||||
EMBEDDINGS_DIMENSION=256
|
||||
|
||||
# === Application ===
|
||||
SECRET_KEY=change-me-in-production-32-bytes-long-min
|
||||
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||
SECRET_KEY=change-me-to-a-random-32-byte-string-min
|
||||
DEBUG=false
|
||||
LOG_LEVEL=INFO
|
||||
CORS_ORIGINS=http://localhost,http://localhost:5173
|
||||
# Comma-separated list of allowed CORS origins (or * for any)
|
||||
CORS_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost
|
||||
|
||||
# === Admin setup ===
|
||||
# Leave empty to auto-generate on first startup (will be printed in backend logs).
|
||||
# Or set explicitly to control the URL: /register/admin?token=<ADMIN_SETUP_TOKEN>
|
||||
ADMIN_SETUP_TOKEN=
|
||||
|
||||
# === Storage ===
|
||||
DATA_DIR=/app/data
|
||||
|
||||
# === Frontend (Vite dev) ===
|
||||
VITE_API_BASE_URL=http://localhost/api
|
||||
# === Frontend (Vite — ONLY used by `npm run dev`, ignored by docker build) ===
|
||||
# The docker build bakes "/api" (relative) into the bundle so the browser
|
||||
# uses the same origin + nginx proxies /api → backend:8000.
|
||||
# For local `npm run dev` (outside docker), set this to the backend URL.
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -4,6 +4,22 @@ All notable changes to AI-RPG are documented here.
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.1] — 2026-06-20
|
||||
|
||||
### Fixed
|
||||
- **docker-compose.yml**: removed obsolete `version: "3.9"` (caused warning in modern Docker Compose).
|
||||
- **docker-compose.yml**: fixed frontend build context — was `./frontend` (broke `COPY deploy/nginx.conf` and `COPY frontend/package*.json` in Dockerfile.frontend). Now correctly `.` (project root) with `dockerfile: deploy/Dockerfile.frontend`.
|
||||
- **docker-compose.yml**: fixed frontend port mapping — was `5173:5173` but the frontend container is nginx on port 80. Now `8080:80` so the app is accessible at `http://localhost:8080`.
|
||||
- **docker-compose.yml**: added `extra_hosts: ["host.docker.internal:host-gateway"]` to the backend service — enables `LLM_API_URL=http://host.docker.internal:11434/v1` to work on Linux hosts (not just Docker Desktop on Mac/Windows).
|
||||
- **docker-compose.yml**: added `VITE_API_BASE_URL: /api` build arg for the frontend service — bakes the relative `/api` URL into the Vite bundle so the browser uses the same origin + nginx proxies `/api` → `backend:8000`.
|
||||
- **deploy/Dockerfile.frontend**: added `ARG VITE_API_BASE_URL=/api` + `ENV` so the build arg actually gets baked into the Vite bundle.
|
||||
- **deploy/nginx.conf**: extended SSE timeouts from 300s → 600s; added `proxy_send_timeout`; added `Upgrade`/`Connection` headers for future WebSocket support; added gzip for static assets.
|
||||
- **.env.example**: clarified `VITE_API_BASE_URL` — only used by local `npm run dev`, ignored by docker build (which uses `/api` relative). Removed the misleading `http://localhost/api` default.
|
||||
- **frontend/src/lib/api.ts**: now respects `VITE_API_BASE_URL` env var with fallback to relative `/api`. Works both for local dev (point at separate backend) and Docker (nginx proxy).
|
||||
- **frontend/src/vite-env.d.ts**: added Vite env type declarations so TypeScript knows about `import.meta.env.VITE_API_BASE_URL`.
|
||||
- **app/api/deps.py**: added `?access_token=<jwt>` query parameter fallback for SSE endpoints. Native `EventSource` cannot send `Authorization` headers, so the frontend SSE client passes the token via query string. Without this fix, all SSE endpoints (`/iterate/stream`, `/builder/stream`, `/editor/stream`) returned 401.
|
||||
- **.dockerignore**: added at project root — excludes `node_modules/`, `__pycache__/`, `.venv/`, `data/`, `.git/`, etc. from Docker build contexts (faster builds, smaller context transfer).
|
||||
|
||||
## [1.0.0] — 2026-06-20
|
||||
|
||||
### Added — Sprint 1: Foundation
|
||||
|
||||
16
README.md
16
README.md
@@ -73,13 +73,15 @@ cp .env.example .env
|
||||
# Отредактируйте SECRET_KEY, ADMIN_SETUP_TOKEN, LLM_API_URL, LLM_API_KEY
|
||||
|
||||
# 2. Поднять всё
|
||||
docker compose up -d
|
||||
docker compose up -d --build
|
||||
|
||||
# 3. Зайти на http://localhost (frontend через nginx)
|
||||
# Или http://localhost:5173 (frontend dev) / http://localhost:8000/api/docs (backend)
|
||||
# 3. Открыть в браузере:
|
||||
# - Frontend (nginx + React build): http://localhost:8080
|
||||
# - Backend Swagger docs: http://localhost:8000/api/docs
|
||||
# - Health-check: http://localhost:8000/api/health
|
||||
```
|
||||
|
||||
При первом старте сервер напечатает в лог:
|
||||
При первом старте backend напечатает в лог:
|
||||
```
|
||||
=== AI-RPG Admin Setup ===
|
||||
No admin user yet. Open this URL in your browser:
|
||||
@@ -87,7 +89,11 @@ No admin user yet. Open this URL in your browser:
|
||||
===========================
|
||||
```
|
||||
|
||||
Откройте `http://localhost/register/admin?token=<...>` и создайте первого админа.
|
||||
Посмотреть лог: `docker compose logs backend | head -20`.
|
||||
|
||||
Откройте `http://localhost:8080/register/admin?token=<...>` и создайте первого админа.
|
||||
|
||||
> **Примечание про LLM:** если у вас Ollama на хосте, используйте `LLM_API_URL=http://host.docker.internal:11434/v1` — backend-контейнер автоматически резолвит `host.docker.internal` через `extra_hosts: host-gateway` (работает на Linux/macOS/Windows).
|
||||
|
||||
### Опция 2: локальный dev (backend + frontend раздельно)
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
"""Shared API dependencies: current user, admin guard, db session, settings."""
|
||||
"""Shared API dependencies: current user, admin guard, db session, settings.
|
||||
|
||||
Token resolution order:
|
||||
1. `Authorization: Bearer <jwt>` header (preferred).
|
||||
2. `?access_token=<jwt>` query parameter (fallback for SSE — EventSource cannot
|
||||
set custom headers).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -20,16 +26,29 @@ _bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
async def get_current_user(
|
||||
creds: HTTPAuthorizationCredentials | None = Depends(_bearer),
|
||||
access_token: str | None = Query(default=None, description="JWT access token (fallback for SSE)"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
"""Resolve the JWT bearer token to a User row.
|
||||
|
||||
Accepts the token either in the `Authorization: Bearer <jwt>` header
|
||||
or as the `?access_token=<jwt>` query parameter (the latter is needed
|
||||
because native `EventSource` cannot send custom headers, and the
|
||||
frontend SSE client passes the token via query string).
|
||||
|
||||
Raises 401 on missing/invalid/expired token.
|
||||
"""
|
||||
if creds is None or creds.scheme.lower() != "bearer":
|
||||
raw_token: str | None = None
|
||||
if creds is not None and creds.scheme.lower() == "bearer":
|
||||
raw_token = creds.credentials
|
||||
elif access_token:
|
||||
raw_token = access_token
|
||||
|
||||
if not raw_token:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token")
|
||||
|
||||
try:
|
||||
payload = decode_token(creds.credentials)
|
||||
payload = decode_token(raw_token)
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {e}")
|
||||
if payload.get("type") != "access":
|
||||
|
||||
@@ -13,10 +13,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Create data directory
|
||||
# Create data directory for assets / uploads
|
||||
RUN mkdir -p /app/data/assets
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Default: run uvicorn; in dev override with `npm run dev` equivalent
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
# Default: run uvicorn. In dev override via docker-compose volumes for hot reload.
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Bake the API base URL into the Vite bundle at build time.
|
||||
# Default is the relative "/api" — works when the app is served from the same
|
||||
# origin as the /api reverse proxy (nginx in front of the backend).
|
||||
ARG VITE_API_BASE_URL=/api
|
||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||
|
||||
# Copy frontend manifest first for better layer caching
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the frontend source and build
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# Serve stage
|
||||
# -------------------------------------------------------------------
|
||||
# Serve stage — nginx
|
||||
# -------------------------------------------------------------------
|
||||
FROM nginx:1.24-alpine
|
||||
|
||||
# Copy built static assets from the build stage
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx config (path is relative to the build context, which is the project root)
|
||||
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -5,12 +5,17 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# SPA fallback
|
||||
# Gzip for text assets
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
|
||||
# SPA fallback — must be before /api/ location
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API + SSE
|
||||
# API + SSE — proxy to backend container
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -19,14 +24,27 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# SSE: disable buffering
|
||||
# SSE: disable buffering, extend timeouts for long-running streams
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
|
||||
# WebSocket support (in case of future upgrade)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# Static assets (icons, uploads)
|
||||
# Static assets (icons, uploads) served by backend
|
||||
location /static/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Health-check passthrough
|
||||
location = /health {
|
||||
proxy_pass http://backend:8000/api/health;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
@@ -39,8 +37,13 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# Override .env values with docker-compose service names
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-airpg}:${POSTGRES_PASSWORD:-airpg}@db:5432/${POSTGRES_DB:-airpg}
|
||||
QDRANT_URL: http://qdrant:6333
|
||||
# Allow backend to reach host services (e.g. Ollama on host) via host.docker.internal
|
||||
# On Linux this requires the extra_hosts mapping below.
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./app:/app/app
|
||||
@@ -53,13 +56,18 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
# nginx-served production build of the React app.
|
||||
# Listens on container port 80, mapped to host port 8080.
|
||||
# Access the app at http://localhost:8080
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: ../deploy/Dockerfile.frontend
|
||||
environment:
|
||||
VITE_API_BASE_URL: /api
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile.frontend
|
||||
args:
|
||||
# Bake the API base URL into the bundle at build time.
|
||||
# Relative "/api" works because nginx proxies /api/* → backend:8000.
|
||||
VITE_API_BASE_URL: /api
|
||||
ports:
|
||||
- "5173:5173"
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
|
||||
@@ -29,7 +29,10 @@ import type {
|
||||
WorldPreset,
|
||||
} from "@/types";
|
||||
|
||||
const BASE_URL = "/api";
|
||||
// Use VITE_API_BASE_URL if set (for local `npm run dev` pointing at a separate
|
||||
// backend); otherwise default to relative "/api" which works behind the nginx
|
||||
// reverse proxy in the Docker deployment.
|
||||
const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, "") || "/api";
|
||||
const ACCESS_TOKEN_KEY = "airpg_access_token";
|
||||
const REFRESH_TOKEN_KEY = "airpg_refresh_token";
|
||||
|
||||
|
||||
9
frontend/src/vite-env.d.ts
vendored
Normal file
9
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/components/admin/IconsPanel.tsx","./src/components/admin/LlmLogsTable.tsx","./src/components/admin/SettingsPanel.tsx","./src/components/admin/StatsPanel.tsx","./src/components/admin/TestButtons.tsx","./src/components/admin/UsersTable.tsx","./src/components/auth/ProtectedRoute.tsx","./src/components/sessions/ActionInput.tsx","./src/components/sessions/ChatView.tsx","./src/components/sessions/PhaseProgress.tsx","./src/components/sessions/SseStatus.tsx","./src/components/sessions/ToolCallBubble.tsx","./src/components/ui/Button.tsx","./src/components/ui/Card.tsx","./src/components/ui/Input.tsx","./src/components/ui/JsonEditor.tsx","./src/components/ui/Modal.tsx","./src/components/ui/Navbar.tsx","./src/components/ui/Spinner.tsx","./src/components/ui/Textarea.tsx","./src/components/ui/Toast.tsx","./src/components/worlds/WorldBuilder.tsx","./src/components/worlds/WorldCard.tsx","./src/components/worlds/WorldEditor.tsx","./src/i18n/index.ts","./src/lib/api.ts","./src/lib/cn.ts","./src/lib/sse.ts","./src/pages/AdminPage.tsx","./src/pages/AdminRegisterPage.tsx","./src/pages/LoginPage.tsx","./src/pages/PlayPage.tsx","./src/pages/RegisterPage.tsx","./src/pages/WorldBuilderPage.tsx","./src/pages/WorldEditPage.tsx","./src/pages/WorldsListPage.tsx","./src/stores/authStore.ts","./src/stores/sessionStore.ts","./src/stores/toastStore.ts","./src/stores/uiStore.ts","./src/stores/worldsStore.ts","./src/types/index.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/admin/IconsPanel.tsx","./src/components/admin/LlmLogsTable.tsx","./src/components/admin/SettingsPanel.tsx","./src/components/admin/StatsPanel.tsx","./src/components/admin/TestButtons.tsx","./src/components/admin/UsersTable.tsx","./src/components/auth/ProtectedRoute.tsx","./src/components/sessions/ActionInput.tsx","./src/components/sessions/ChatView.tsx","./src/components/sessions/PhaseProgress.tsx","./src/components/sessions/SseStatus.tsx","./src/components/sessions/ToolCallBubble.tsx","./src/components/ui/Button.tsx","./src/components/ui/Card.tsx","./src/components/ui/Input.tsx","./src/components/ui/JsonEditor.tsx","./src/components/ui/Modal.tsx","./src/components/ui/Navbar.tsx","./src/components/ui/Spinner.tsx","./src/components/ui/Textarea.tsx","./src/components/ui/Toast.tsx","./src/components/worlds/WorldBuilder.tsx","./src/components/worlds/WorldCard.tsx","./src/components/worlds/WorldEditor.tsx","./src/i18n/index.ts","./src/lib/api.ts","./src/lib/cn.ts","./src/lib/sse.ts","./src/pages/AdminPage.tsx","./src/pages/AdminRegisterPage.tsx","./src/pages/LoginPage.tsx","./src/pages/PlayPage.tsx","./src/pages/RegisterPage.tsx","./src/pages/WorldBuilderPage.tsx","./src/pages/WorldEditPage.tsx","./src/pages/WorldsListPage.tsx","./src/stores/authStore.ts","./src/stores/sessionStore.ts","./src/stores/toastStore.ts","./src/stores/uiStore.ts","./src/stores/worldsStore.ts","./src/types/index.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user