diff --git a/.env.example b/.env.example index e48625b..dac58eb 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,6 @@ LAYA_ADMIN_USERNAME=admin # Set a password of at least 10 characters, or use LAYA_ADMIN_PASSWORD_HASH instead. LAYA_ADMIN_PASSWORD='' # LAYA_ADMIN_PASSWORD_HASH='$argon2id$...' -LAYA_PUBLIC_ORIGIN=https://console.example.com LAYA_SESSION_HOURS=12 LAYA_MAX_LOADED_MODELS=1 # LAYA_DEVICE=cpu diff --git a/README.md b/README.md index 340028d..c0dba3d 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,11 @@ sh scripts/check-upstream.sh ## 管理员与配置 -在 `.env` 中设置 `LAYA_ADMIN_USERNAME` 和至少 10 个字符的 `LAYA_ADMIN_PASSWORD`,服务启动时会在内存中生成 Argon2id 哈希用于登录校验。也可以不设置明文密码,改用 `.venv/bin/python scripts/hash-password.py` 生成 `LAYA_ADMIN_PASSWORD_HASH`;两者必须且只能设置一个。哈希值用单引号包住,确保 Docker Compose 按字面保留 `$`。`LAYA_PUBLIC_ORIGIN` 也必须填写;生产环境必须是 HTTPS 来源,例如 `https://console.example.com`。本地 HTTP 测试需要 `LAYA_ALLOW_INSECURE_LOCAL=1`。`.env` 已被 Git 忽略,不要提交实际密码。 +Docker Compose 只传入当前 shell 或部署平台设置的 `LAYA_ADMIN_USERNAME` 和 `LAYA_ADMIN_PASSWORD`,不需要 `.env` 文件。密码至少 10 个字符;服务启动时会在内存中生成 Argon2id 哈希用于登录校验。管理操作仍要求会话和 CSRF token。其他运行参数使用后端默认值。`.env.example` 仅供本地热更新脚本使用,不要提交实际密码。 ```sh -cp .env.example .env python3.12 -m venv .venv .venv/bin/python -m pip install -e 'backend[test]' -# 编辑 .env,填写 LAYA_ADMIN_USERNAME 和 LAYA_ADMIN_PASSWORD ``` ## 模型文件 @@ -48,13 +46,14 @@ LAYA_MODEL_DIR=models .venv/bin/python scripts/smoke-real-model.py 在 GitHub 仓库的 **Settings → Secrets and variables → Actions** 配置 `DOCKERHUB_USERNAME` 和 `DOCKERHUB_TOKEN`(需要有 `1panel/laya-server` 的推送权限)。在 **Actions → Build and push LAYA SERVER → Run workflow** 输入版本标签。正式发布时可同时勾选 `latest`;测试标签保持关闭。工作流会检出被忽略的上游 v0.3.7 源码并校验 SHA,运行后端测试,再构建及推送镜像。 -拉取已发布镜像并启动: +在当前 shell 中设置变量,拉取已发布镜像并启动: ```sh -cp .env.example .env -# 编辑 .env,配置管理员账号、密码和 LAYA_PUBLIC_ORIGIN -LAYA_IMAGE_TAG=dev docker compose pull -LAYA_IMAGE_TAG=dev docker compose up -d +export LAYA_ADMIN_USERNAME=admin +export LAYA_ADMIN_PASSWORD='replace-with-a-password-of-at-least-10-characters' +export LAYA_IMAGE_TAG=dev +docker compose pull +docker compose up -d ``` 也可用本地已检出的上游源码构建: @@ -62,19 +61,21 @@ LAYA_IMAGE_TAG=dev docker compose up -d ```sh sh scripts/check-upstream.sh docker build -t 1panel/laya-server:dev . -LAYA_IMAGE_TAG=dev docker compose up -d +export LAYA_IMAGE_TAG=dev +docker compose up -d ``` 如使用 `latest`,直接执行: ```sh +unset LAYA_IMAGE_TAG docker compose pull docker compose up -d ``` -Compose 只启动一个应用服务并将 `127.0.0.1:8080` 暴露给宿主机。公网入口需由外部反向代理提供 HTTPS,并将请求转发到该端口。SQLite 数据在 `laya-data` 卷;更新容器不会丢失数据库。构建机器需要能访问 PyPI、PyTorch CPU 包索引、npm registry 和 Hugging Face;已构建镜像启动时无需拉取源码、依赖或模型。 +Compose 从启动它的进程环境传入管理员配置,只启动一个应用服务并将 `127.0.0.1:8080` 暴露给宿主机。公网入口需由外部反向代理提供 HTTPS,并将请求转发到该端口。SQLite 数据在 `laya-data` 卷;更新容器不会丢失数据库。构建机器需要能访问 PyPI、PyTorch CPU 包索引、npm registry 和 Hugging Face;已构建镜像启动时无需拉取源码、依赖或模型。 -如果由现有的 1Panel 反向代理提供公网 HTTPS,将域名请求转发到宿主机的 `127.0.0.1:8080`,并确保 `LAYA_PUBLIC_ORIGIN` 与实际 HTTPS 域名一致。反向代理不属于本项目的应用容器。 +如果由现有的 1Panel 反向代理提供公网 HTTPS,将域名请求转发到宿主机的 `127.0.0.1:8080`。登录时服务根据请求是否为 HTTPS 设置 Cookie 的 `Secure` 标记;请让反向代理正确转发协议。反向代理不属于本项目的应用容器。 ### 本地开发(热更新) @@ -95,7 +96,7 @@ pnpm install --frozen-lockfile pnpm dev ``` -打开 `http://127.0.0.1:5173` 登录。Vite 将 `/internal` 和 `/v1` 代理到本地 8000 端口;生产环境仍由同一个 FastAPI 容器提供构建后的前端。若只想用一个本地进程,可先在 `frontend/` 运行 `pnpm build`,再把 `LAYA_PUBLIC_ORIGIN` 设为 `http://127.0.0.1:8000` 启动 Uvicorn 并打开 8000 端口。 +打开 `http://127.0.0.1:5173` 登录。Vite 将 `/internal` 和 `/v1` 代理到本地 8000 端口;生产环境仍由同一个 FastAPI 容器提供构建后的前端。若只想用一个本地进程,可先在 `frontend/` 运行 `pnpm build`,再启动 Uvicorn 并打开 8000 端口。 控制台支持简体中文、English 和繁體中文。登录页及登录后的顶部栏均可切换语言;首次访问按浏览器语言选择,手动选择会保存在当前浏览器中。 diff --git a/README_es.md b/README_es.md index e8f4301..0f9b153 100644 --- a/README_es.md +++ b/README_es.md @@ -16,20 +16,18 @@ sh scripts/check-upstream.sh ## Administrador y configuración -Define `LAYA_ADMIN_USERNAME` y `LAYA_ADMIN_PASSWORD` (de al menos 10 caracteres) en `.env`; al iniciar el servicio se genera en memoria un hash Argon2id para validar el inicio de sesión. También puedes omitir la contraseña en claro y generar `LAYA_ADMIN_PASSWORD_HASH` con `.venv/bin/python scripts/hash-password.py`; se debe establecer exactamente una de las dos. Envuelve el valor del hash entre comillas simples para que Docker Compose conserve `$` literalmente. `LAYA_PUBLIC_ORIGIN` también es obligatorio; en producción debe ser un origen HTTPS, por ejemplo `https://console.example.com`. Para pruebas locales por HTTP se necesita `LAYA_ALLOW_INSECURE_LOCAL=1`. `.env` está ignorado por Git: no confirmes (commit) contraseñas reales. +Docker Compose recibe solo `LAYA_ADMIN_USERNAME` y `LAYA_ADMIN_PASSWORD` del shell o de la plataforma de despliegue; no necesita un archivo `.env`. La contraseña debe tener al menos 10 caracteres. Las operaciones administrativas siguen requiriendo sesión y token CSRF. Los demás parámetros usan los valores predeterminados del backend. `.env.example` se usa solo para el script de desarrollo local; no confirmes contraseñas reales. ```sh -cp .env.example .env python3.12 -m venv .venv .venv/bin/python -m pip install -e 'backend[test]' -# Edita .env y rellena LAYA_ADMIN_USERNAME y LAYA_ADMIN_PASSWORD ``` ## Archivos de modelo -Los pesos de los modelos no se distribuyen con el repositorio ni con la imagen. `scripts/download-models.py` fija el repositorio de Hugging Face en el commit `1c5edc17a7acd8701df6fc341c0d179f1c62c982` y coloca los tres checkpoints en `/models/english`, `/models/multilingual` y `/models/typed-decisions` del volumen de modelos persistente. Cada directorio debe contener como mínimo `rl_agent_config.json`, `model.safetensors`, `tokenizer/` y `encoder/` del paquete de modelo upstream. La presencia de los archivos se comprueba con `GET /health/ready`; si faltan, la inferencia devuelve `503 MODEL_UNAVAILABLE`. Sigue siendo necesario hacer una prueba de humo de inferencia para confirmar que los modelos son realmente compatibles. +El repositorio no incluye pesos. El Dockerfile descarga el checkpoint **multilingual** desde el commit fijo de Hugging Face `1c5edc17a7acd8701df6fc341c0d179f1c62c982` durante la compilación y lo incorpora a `/opt/models/multilingual` en la imagen final. La compilación comprueba la inferencia sin conexión en inglés y chino. Al arrancar el contenedor no hay que descargar ni montar modelos. `GET /health/ready` comprueba los archivos incluidos. -La inferencia de modelos de este proyecto no se descarga automáticamente al arrancar la aplicación; prepara el volumen de modelos antes del arranque. Un proceso de aplicación solo carga los modelos necesarios y, de forma predeterminada, conserva como máximo uno en memoria; ajusta el valor con `LAYA_MAX_LOADED_MODELS`. Este valor controla cuántos modelos mantiene la caché, no cuántas solicitudes se procesan simultáneamente. El servicio no define ranuras adicionales de concurrencia de inferencia; la concurrencia real depende del grupo de hilos de ejecución, de los modelos y de los recursos de la máquina. La imagen de inferencia por CPU usa la wheel de PyTorch para CPU; si despliegas en GPU, cambia a la base de PyTorch adecuada para tu dispositivo y valida el resultado. +La imagen publicada usa `LAYA_MODEL_PROFILE=multilingual`: `model=auto` y `model=multilingual` utilizan ese checkpoint; solicitar `english` o `typed-decisions` devuelve `422 MODEL_NOT_AVAILABLE`. `LAYA_MAX_LOADED_MODELS` controla cuántos modelos permanecen en memoria, no el número de solicitudes simultáneas. La imagen usa PyTorch para CPU y el workflow construye `linux/amd64`. En local, instala primero las dependencias de ejecución upstream y el checkout de Laya ignorado por Git, descarga los tres modelos en sus versiones fijas y ejecuta una prueba de humo con solicitudes reales que cubra inglés, selección explícita en chino, enrutado automático en chino y typed-decisions: @@ -46,17 +44,19 @@ La descarga de modelos y la ejecución dependen de PyTorch, Transformers, Safete ## Arranque -Antes del primer arranque, construye la imagen y descarga los modelos de versión fija en el volumen de modelos; después inicia la aplicación: +Exporta las variables desde el shell y arranca la imagen publicada: ```sh -docker compose build -docker compose run --rm app python /app/scripts/download-models.py +export LAYA_ADMIN_USERNAME=admin +export LAYA_ADMIN_PASSWORD='replace-with-a-password-of-at-least-10-characters' +export LAYA_IMAGE_TAG=dev +docker compose pull docker compose up -d ``` -Compose solo inicia un servicio de aplicación y expone `127.0.0.1:8080` al host. La entrada de acceso público debe proveer HTTPS mediante un proxy inverso externo que reenvíe las solicitudes a ese puerto. Los datos de SQLite están en el volumen `laya-data` y los modelos en el volumen `laya-models`. La máquina de compilación necesita acceso a PyPI, al índice de paquetes PyTorch para CPU y al registry de npm; al arrancar una imagen ya compilada no hace falta descargar código fuente ni dependencias. +Compose transmite las variables del proceso que lo ejecuta, inicia un único servicio y expone `127.0.0.1:8080` al host. Un proxy inverso externo debe proporcionar HTTPS y reenviar las solicitudes a ese puerto. Los datos de SQLite se conservan en el volumen `laya-data`. La máquina de compilación necesita acceso a PyPI, al índice de PyTorch para CPU, al registry de npm y a Hugging Face; la imagen ya compilada no descarga código, dependencias ni modelos al arrancar. -Si un proxy inverso 1Panel existente provee el HTTPS público, reenvía las solicitudes del dominio a `127.0.0.1:8080` del host y asegúrate de que `LAYA_PUBLIC_ORIGIN` coincida con el dominio HTTPS real. El proxy inverso no forma parte del contenedor de aplicación de este proyecto. +Si un proxy inverso 1Panel existente provee el HTTPS público, reenvía las solicitudes del dominio a `127.0.0.1:8080` del host. Durante el inicio de sesión, el servicio establece el atributo `Secure` de las cookies según si la solicitud usa HTTPS; configura el proxy para que transmita correctamente el protocolo. El proxy inverso no forma parte del contenedor de aplicación de este proyecto. ### Desarrollo local (recarga en caliente) @@ -75,7 +75,7 @@ cd frontend pnpm dev ``` -Abre `http://127.0.0.1:5173` e inicia sesión. Vite redirige `/internal` y `/v1` al puerto local 8000; en producción el frontend compilado lo sigue sirviendo el mismo contenedor de FastAPI. Si solo quieres un proceso local, ejecuta primero `pnpm build` en `frontend/`, establece `LAYA_PUBLIC_ORIGIN` en `http://127.0.0.1:8000`, arranca Uvicorn y abre el puerto 8000. +Abre `http://127.0.0.1:5173` e inicia sesión. Vite redirige `/internal` y `/v1` al puerto local 8000; en producción el frontend compilado lo sigue sirviendo el mismo contenedor de FastAPI. Si solo quieres un proceso local, ejecuta primero `pnpm build` en `frontend/`, arranca Uvicorn y abre el puerto 8000. La consola admite chino simplificado, inglés y chino tradicional. Puedes cambiar el idioma tanto en la página de inicio de sesión como en la barra superior una vez dentro; en la primera visita se elige el idioma del navegador y la elección manual se guarda en el navegador actual. @@ -90,7 +90,7 @@ curl -X POST https://console.example.com/v1/systemone \ -d '{"state":{"message":"I was charged twice"},"questions":{"refund":{"type":"noul","instructions":"Does the customer ask for a refund?"}}}' ``` -En la solicitud, `state` puede ser una cadena, un objeto JSON o un array; `questions` es un mapa de identificadores de pregunta no vacío que admite `noul`, `choice` y `score`; `model` admite `auto` (predeterminado), `english`, `multilingual` o `typed-decisions`. La respuesta conserva `answers`, `model` y `usage` de upstream. Los errores usan `detail.code` y `detail.message`. Una clave no válida devuelve 401, un fallo de validación 422 y un modelo no disponible 503. El Playground de la consola usa la sesión de administrador y se registra como una fuente de consumo independiente. +En la solicitud, `state` puede ser una cadena, un objeto JSON o un array; `questions` es un mapa no vacío que admite `noul`, `choice` y `score`. En la imagen publicada, `model` admite `auto` (predeterminado) o `multilingual`; pedir otro modelo devuelve `422 MODEL_NOT_AVAILABLE`. El desarrollo local admite los tres modelos si se han descargado. La respuesta conserva `answers`, `model` y `usage` de upstream. Los errores usan `detail.code` y `detail.message`. Una clave no válida devuelve 401, un fallo de validación 422 y un modelo no disponible 503. El Playground de la consola usa la sesión de administrador y se registra como una fuente de consumo independiente. ## Datos y mantenimiento diff --git a/backend/server/config.py b/backend/server/config.py index ec102ad..c722ecd 100644 --- a/backend/server/config.py +++ b/backend/server/config.py @@ -10,8 +10,6 @@ class Settings: admin_username: str admin_password_hash: str database_path: Path - public_origin: str - secure_cookie: bool session_hours: int model_dir: Path device: str | None @@ -24,28 +22,20 @@ def from_env(cls) -> "Settings": username = os.environ.get("LAYA_ADMIN_USERNAME", "") password = os.environ.get("LAYA_ADMIN_PASSWORD", "") password_hash = os.environ.get("LAYA_ADMIN_PASSWORD_HASH", "") - origin = os.environ.get("LAYA_PUBLIC_ORIGIN", "").rstrip("/") - if not username or not origin: - raise RuntimeError("Set LAYA_ADMIN_USERNAME and LAYA_PUBLIC_ORIGIN") + if not username: + raise RuntimeError("Set LAYA_ADMIN_USERNAME") if bool(password) == bool(password_hash): raise RuntimeError("Set exactly one of LAYA_ADMIN_PASSWORD or LAYA_ADMIN_PASSWORD_HASH") if password: if len(password) < 10: raise RuntimeError("LAYA_ADMIN_PASSWORD must contain at least 10 characters") password_hash = PasswordHasher().hash(password) - if not origin.startswith(("https://", "http://")): - raise RuntimeError("LAYA_PUBLIC_ORIGIN must be an absolute HTTP(S) origin") - if origin.startswith("http://") and os.environ.get("LAYA_ALLOW_INSECURE_LOCAL") != "1": - raise RuntimeError("HTTP origin requires LAYA_ALLOW_INSECURE_LOCAL=1") - if any(char in origin.split("://", 1)[1] for char in "/?#"): - raise RuntimeError("LAYA_PUBLIC_ORIGIN must not contain a path, query, or fragment") model_profile = os.environ.get("LAYA_MODEL_PROFILE", "all") if model_profile not in ("all", "multilingual"): raise RuntimeError("LAYA_MODEL_PROFILE must be all or multilingual") return cls( username, password_hash, Path(os.environ.get("LAYA_DATABASE_PATH", "/data/laya.sqlite3")), - origin, origin.startswith("https://"), int(os.environ.get("LAYA_SESSION_HOURS", "12")), Path(os.environ.get("LAYA_MODEL_DIR", "/models")), os.environ.get("LAYA_DEVICE") or None, diff --git a/backend/server/main.py b/backend/server/main.py index af2f350..d94fdd4 100644 --- a/backend/server/main.py +++ b/backend/server/main.py @@ -49,10 +49,6 @@ def validation_error(request: Request, exc: RequestValidationError) -> JSONRespo "code": "VALIDATION_ERROR", "message": "Request validation failed", "errors": fields, }}) - def require_origin(request: Request) -> None: - if request.headers.get("origin") != settings.public_origin: - raise error(403, "ORIGIN_MISMATCH", "Request origin is not allowed") - def current_session(request: Request) -> str: token = request.cookies.get(SESSION_COOKIE) if not token: @@ -69,7 +65,6 @@ def current_session(request: Request) -> str: Session = Annotated[str, Depends(current_session)] def require_csrf(request: Request, session: Session, x_csrf_token: Annotated[str | None, Header()] = None) -> None: - require_origin(request) cookie = request.cookies.get(CSRF_COOKIE) if not cookie or not x_csrf_token or not hmac.compare_digest(cookie, x_csrf_token): raise error(403, "CSRF_INVALID", "CSRF token is invalid") @@ -163,7 +158,6 @@ def ready() -> dict[str, str]: @app.post("/internal/auth/login") def login(payload: LoginRequest, request: Request, response: Response) -> dict[str, str]: - require_origin(request) source = request.client.host if request.client else "unknown" since = (datetime.now(timezone.utc) - timedelta(minutes=15)).isoformat(timespec="seconds") with db.connect() as connection: @@ -188,7 +182,8 @@ def login(payload: LoginRequest, request: Request, response: Response) -> dict[s expires = (datetime.now(timezone.utc) + timedelta(hours=settings.session_hours)).isoformat(timespec="seconds") with db.connect() as connection: connection.execute("INSERT INTO sessions VALUES (?,?,?)", (digest(session), digest(csrf), expires)) - cookie_options = {"secure": settings.secure_cookie, "samesite": "lax", "path": "/", "max_age": settings.session_hours * 3600} + is_https = request.url.scheme == "https" or request.headers.get("origin", "").startswith("https://") + cookie_options = {"secure": is_https, "samesite": "lax", "path": "/", "max_age": settings.session_hours * 3600} response.set_cookie(SESSION_COOKIE, session, httponly=True, **cookie_options) response.set_cookie(CSRF_COOKIE, csrf, httponly=False, **cookie_options) return {"username": settings.admin_username} diff --git a/backend/tests/test_flow.py b/backend/tests/test_flow.py index 0669f45..b30bfcb 100644 --- a/backend/tests/test_flow.py +++ b/backend/tests/test_flow.py @@ -9,8 +9,6 @@ os.environ.setdefault("LAYA_ADMIN_USERNAME", "test-admin") os.environ.setdefault("LAYA_ADMIN_PASSWORD_HASH", PasswordHasher().hash("test-password")) -os.environ.setdefault("LAYA_PUBLIC_ORIGIN", "http://testserver") -os.environ.setdefault("LAYA_ALLOW_INSECURE_LOCAL", "1") os.environ.setdefault("LAYA_DATABASE_PATH", "/tmp/laya-server-import-test.sqlite3") from server.config import Settings # noqa: E402 @@ -51,7 +49,7 @@ def test_multilingual_image_routes_auto_and_rejects_unbundled_models(tmp_path): path.touch() fake = FakePredictor() settings = Settings("admin", PasswordHasher().hash("correct horse battery staple"), tmp_path / "db.sqlite3", - "http://testserver", False, 1, tmp_path / "models", None, 1, tmp_path / "missing-dist", "multilingual") + 1, tmp_path / "models", None, 1, tmp_path / "missing-dist", "multilingual") client = TestClient(create_app(settings, fake)) assert client.get("/health/ready").status_code == 200 assert client.get("/internal/models").status_code == 401 @@ -75,8 +73,7 @@ def test_admin_password_from_environment(tmp_path, monkeypatch): monkeypatch.setenv("LAYA_ADMIN_USERNAME", "configured-admin") monkeypatch.setenv("LAYA_ADMIN_PASSWORD", password) monkeypatch.setenv("LAYA_ADMIN_PASSWORD_HASH", "") - monkeypatch.setenv("LAYA_PUBLIC_ORIGIN", "http://testserver") - monkeypatch.setenv("LAYA_ALLOW_INSECURE_LOCAL", "1") + monkeypatch.delenv("LAYA_PUBLIC_ORIGIN", raising=False) monkeypatch.setenv("LAYA_DATABASE_PATH", str(tmp_path / "db.sqlite3")) settings = Settings.from_env() monkeypatch.setenv("LAYA_MODEL_PROFILE", "multilingual") @@ -91,9 +88,21 @@ def test_admin_password_from_environment(tmp_path, monkeypatch): response = client.post( "/internal/auth/login", json={"username": "configured-admin", "password": password}, - headers={"Origin": "http://testserver"}, ) assert response.status_code == 200 + assert all("; secure" not in value.lower() for value in response.headers.get_list("set-cookie")) + csrf = client.get("/internal/auth/session").json()["csrf_token"] + assert client.post("/internal/api-keys", json={"name": "without-origin"}, + headers={"X-CSRF-Token": csrf}).status_code == 201 + + https_client = TestClient(create_app(settings, FakePredictor()), base_url="https://testserver") + secure_login = https_client.post("/internal/auth/login", json={"username": "configured-admin", "password": password}) + assert secure_login.status_code == 200 + assert all("; secure" in value.lower() for value in secure_login.headers.get_list("set-cookie")) + proxied_login = client.post("/internal/auth/login", json={"username": "configured-admin", "password": password}, + headers={"Origin": "https://console.example.com"}) + assert proxied_login.status_code == 200 + assert all("; secure" in value.lower() for value in proxied_login.headers.get_list("set-cookie")) monkeypatch.setenv("LAYA_ADMIN_PASSWORD_HASH", PasswordHasher().hash(password)) with pytest.raises(RuntimeError, match="exactly one"): @@ -112,7 +121,7 @@ def test_login_key_inference_usage_revoke_logout(tmp_path): fake = FakePredictor() db_path = tmp_path / "db.sqlite3" settings = Settings("admin", PasswordHasher().hash("correct horse battery staple"), db_path, - "http://testserver", False, 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") + 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") client = TestClient(create_app(settings, fake)) origin = {"Origin": "http://testserver"} @@ -122,7 +131,6 @@ def test_login_key_inference_usage_revoke_logout(tmp_path): csrf = client.get("/internal/auth/session").json()["csrf_token"] assert csrf assert client.post("/internal/api-keys", json={"name": "production"}, headers=origin).status_code == 403 - assert client.post("/internal/api-keys", json={"name": "production"}, headers={"X-CSRF-Token": csrf}).status_code == 403 assert client.post("/internal/api-keys", json={"name": "production"}, headers={**origin, "X-CSRF-Token": "incorrect"}).status_code == 403 write_headers = {**origin, "X-CSRF-Token": csrf} created = client.post("/internal/api-keys", json={"name": "production"}, headers=write_headers) @@ -189,7 +197,7 @@ def predict(self, state, questions, model=None): db_path = tmp_path / "db.sqlite3" settings = Settings("admin", PasswordHasher().hash("password"), db_path, - "http://testserver", False, 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") + 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") client = TestClient(create_app(settings, UnavailablePredictor())) origin = {"Origin": "http://testserver"} assert client.get("/health/ready").json()["detail"]["code"] == "MODEL_UNAVAILABLE" @@ -218,7 +226,7 @@ def predict(self, state, questions, model=None): def test_login_after_session_expiry(tmp_path, keep_cookies): db_path = tmp_path / "db.sqlite3" settings = Settings("admin", PasswordHasher().hash("test-password"), db_path, - "http://testserver", False, 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") + 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") client = TestClient(create_app(settings, FakePredictor())) credentials = {"username": "admin", "password": "test-password"} origin = {"Origin": "http://testserver"} @@ -263,7 +271,7 @@ def predict(self, state, questions, model=None): return super().predict(state, questions, model) settings = Settings("admin", PasswordHasher().hash("test-password"), tmp_path / "db.sqlite3", - "http://testserver", False, 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") + 1, tmp_path / "models", None, 1, tmp_path / "missing-dist") app = create_app(settings, ConcurrentPredictor()) client = TestClient(app) origin = {"Origin": "http://testserver"} diff --git a/compose.yaml b/compose.yaml index 01e56e3..015fec7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,7 +1,9 @@ services: app: image: 1panel/laya-server:${LAYA_IMAGE_TAG:-latest} - env_file: .env + environment: + - LAYA_ADMIN_USERNAME + - LAYA_ADMIN_PASSWORD ports: - "127.0.0.1:8080:8080" volumes: diff --git a/frontend/src/i18n.tsx b/frontend/src/i18n.tsx index 9b89791..7c8ae34 100644 --- a/frontend/src/i18n.tsx +++ b/frontend/src/i18n.tsx @@ -129,7 +129,6 @@ const zhCN = { docsNote: "首版按成功请求计量;超时重试可能产生新的推理请求。", errorGeneric: "请求失败,请稍后重试。", errorValidation: "请求格式有误,请检查输入。", - errorOrigin: "请求来源不被允许,请从控制台页面重试。", errorUnauthenticated: "会话已失效,请重新登录。", errorCsrf: "请求校验失败,请刷新页面后重试。", errorInvalidKey: "API Key 无效或已撤销。", @@ -270,7 +269,6 @@ const en: Record = { docsNote: "Only successful requests are metered in this version. Retrying after a timeout may run inference again.", errorGeneric: "Request failed. Please try again later.", errorValidation: "Invalid request. Check your input.", - errorOrigin: "Request origin is not allowed. Try again from the console.", errorUnauthenticated: "Your session has expired. Sign in again.", errorCsrf: "Request verification failed. Refresh the page and try again.", errorInvalidKey: "The API key is invalid or revoked.", @@ -409,7 +407,6 @@ const zhTW: Record = { docsNote: "首版依成功請求計量;逾時後重試可能再次執行推論。", errorGeneric: "請求失敗,請稍後再試。", errorValidation: "請求格式有誤,請檢查輸入。", - errorOrigin: "不允許此請求來源,請從控制台重試。", errorUnauthenticated: "工作階段已失效,請重新登入。", errorCsrf: "請求驗證失敗,請重新整理頁面後再試。", errorInvalidKey: "API Key 無效或已撤銷。", @@ -473,7 +470,6 @@ export function useI18n(): I18nContextValue { const errorMessages: Record = { VALIDATION_ERROR: "errorValidation", - ORIGIN_MISMATCH: "errorOrigin", UNAUTHENTICATED: "errorUnauthenticated", CSRF_INVALID: "errorCsrf", INVALID_API_KEY: "errorInvalidKey", diff --git a/scripts/dev-backend.sh b/scripts/dev-backend.sh index 073645c..7e85b51 100644 --- a/scripts/dev-backend.sh +++ b/scripts/dev-backend.sh @@ -20,8 +20,6 @@ if [ -z "${LAYA_ADMIN_PASSWORD:-}" ] && [ -z "${LAYA_ADMIN_PASSWORD_HASH:-}" ]; fi # Vite proxies /internal and /v1 to this backend during local development. -export LAYA_PUBLIC_ORIGIN=http://127.0.0.1:5173 -export LAYA_ALLOW_INSECURE_LOCAL=1 export LAYA_DATABASE_PATH=./data/laya.sqlite3 export LAYA_MODEL_DIR=./models export LAYA_DEVICE="${LAYA_DEVICE:-cpu}" diff --git a/scripts/smoke-real-model.py b/scripts/smoke-real-model.py index 166b49c..31bcb45 100644 --- a/scripts/smoke-real-model.py +++ b/scripts/smoke-real-model.py @@ -14,8 +14,6 @@ os.environ["LAYA_ADMIN_USERNAME"] = "smoke-admin" os.environ.pop("LAYA_ADMIN_PASSWORD", None) os.environ["LAYA_ADMIN_PASSWORD_HASH"] = PasswordHasher().hash(password) - os.environ["LAYA_PUBLIC_ORIGIN"] = "http://testserver" - os.environ["LAYA_ALLOW_INSECURE_LOCAL"] = "1" os.environ["LAYA_DATABASE_PATH"] = str(Path(temporary) / "smoke.sqlite3") os.environ["LAYA_MODEL_DIR"] = os.environ.get("LAYA_MODEL_DIR", "models") os.environ.setdefault("HF_HOME", str(Path(os.environ["LAYA_MODEL_DIR"]) / ".cache"))