From b52fd8741059562ab236493acefc88841797419e Mon Sep 17 00:00:00 2001 From: tanghang97 <1769122446@qq.com> Date: Wed, 27 May 2026 09:51:13 +0800 Subject: [PATCH 1/3] fix: Solving the problem of "make dev" failing to start in Windows environment --- backend/app/gateway/app.py | 33 ++++++++++++++++++++++----------- backend/sitecustomize.py | 21 +++++++++++++++++++++ scripts/serve.sh | 30 ++++++++++++++++-------------- scripts/wait-for-port.sh | 6 ++++++ 4 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 backend/sitecustomize.py diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index 8baecb3631..08f755f0e8 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -1,8 +1,25 @@ import asyncio import logging +import sys from collections.abc import AsyncGenerator from contextlib import asynccontextmanager + +def _configure_windows_event_loop_policy() -> None: + """Use an event loop policy compatible with async psycopg on Windows.""" + if sys.platform != "win32": + return + + selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) + if selector_policy is None: + return + + if not isinstance(asyncio.get_event_loop_policy(), selector_policy): + asyncio.set_event_loop_policy(selector_policy()) + + +_configure_windows_event_loop_policy() + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -161,16 +178,10 @@ async def _migrate_orphaned_threads(store, admin_user_id: str) -> int: async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Application lifespan handler.""" - # Load config and check necessary environment variables at startup. - # `startup_config` is a local snapshot used only for one-shot bootstrap - # work (logging level, langgraph_runtime engines, channels). Request-time - # config resolution always routes through `get_app_config()` in - # `app/gateway/deps.py::get_config()` so `config.yaml` edits become - # visible without a process restart. We deliberately do NOT cache this - # snapshot on `app.state` to keep that contract enforceable. + # Load config and check necessary environment variables at startup try: - startup_config = get_app_config() - apply_logging_level(startup_config.log_level) + app.state.config = get_app_config() + apply_logging_level(app.state.config.log_level) logger.info("Configuration loaded successfully") except Exception as e: error_msg = f"Failed to load configuration during gateway startup: {e}" @@ -180,7 +191,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: logger.info(f"Starting API Gateway on {config.host}:{config.port}") # Initialize LangGraph runtime components (StreamBridge, RunManager, checkpointer, store) - async with langgraph_runtime(app, startup_config): + async with langgraph_runtime(app): logger.info("LangGraph runtime initialised") # Check admin bootstrap state and migrate orphan threads after admin exists. @@ -191,7 +202,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: try: from app.channels.service import start_channel_service - channel_service = await start_channel_service(startup_config) + channel_service = await start_channel_service(app.state.config) logger.info("Channel service started: %s", channel_service.get_status()) except Exception: logger.exception("No IM channels configured or channel service failed to start") diff --git a/backend/sitecustomize.py b/backend/sitecustomize.py new file mode 100644 index 0000000000..b4f2e59739 --- /dev/null +++ b/backend/sitecustomize.py @@ -0,0 +1,21 @@ +"""Process-wide Python startup customizations for local development.""" + +from __future__ import annotations + +import asyncio +import sys + + +def _configure_windows_event_loop_policy() -> None: + if sys.platform != "win32": + return + + selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) + if selector_policy is None: + return + + if not isinstance(asyncio.get_event_loop_policy(), selector_policy): + asyncio.set_event_loop_policy(selector_policy()) + + +_configure_windows_event_loop_policy() diff --git a/scripts/serve.sh b/scripts/serve.sh index 485c9b5fe7..bdab082ea9 100755 --- a/scripts/serve.sh +++ b/scripts/serve.sh @@ -37,6 +37,17 @@ if [ -f "$REPO_ROOT/.env" ]; then set +a fi +_pick_python() { + local candidate + for candidate in python3 python py; do + if command -v "$candidate" >/dev/null 2>&1 && "$candidate" -c 'import sys' >/dev/null 2>&1; then + printf '%s\n' "$candidate" + return 0 + fi + done + return 1 +} + # ── Argument parsing ───────────────────────────────────────────────────────── DEV_MODE=true @@ -218,11 +229,7 @@ fi if $DEV_MODE; then FRONTEND_CMD="pnpm run dev" else - if command -v python3 >/dev/null 2>&1; then - PYTHON_BIN="python3" - elif command -v python >/dev/null 2>&1; then - PYTHON_BIN="python" - else + if ! PYTHON_BIN="$(_pick_python)"; then echo "Python is required to generate BETTER_AUTH_SECRET." exit 1 fi @@ -259,15 +266,10 @@ fi # ── Install dependencies ──────────────────────────────────────────────────── -# Pick a Python for the extras detector. Falls back to plain `python` for -# Windows/Git Bash where only `python` is on PATH. -if command -v python3 >/dev/null 2>&1; then - DETECT_PYTHON="python3" -elif command -v python >/dev/null 2>&1; then - DETECT_PYTHON="python" -else - DETECT_PYTHON="" -fi +# Pick a runnable Python for the extras detector. On Windows/Git Bash, +# `python3` can resolve to the Microsoft Store alias in WindowsApps, which is +# present on PATH but not executable from Bash. +DETECT_PYTHON="$(_pick_python || true)" # Resolve uv extras (postgres, etc.) from UV_EXTRAS or config.yaml so that # `uv sync` does not wipe out optional dependencies on every restart. See diff --git a/scripts/wait-for-port.sh b/scripts/wait-for-port.sh index dc0dffa1d3..0764d8491c 100755 --- a/scripts/wait-for-port.sh +++ b/scripts/wait-for-port.sh @@ -21,6 +21,12 @@ elapsed=0 interval=1 is_port_listening() { + if command -v powershell.exe >/dev/null 2>&1; then + if powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "\$ErrorActionPreference='SilentlyContinue'; if (Get-NetTCPConnection -LocalPort $PORT -State Listen) { exit 0 } else { exit 1 }" >/dev/null 2>&1; then + return 0 + fi + fi + if command -v lsof >/dev/null 2>&1; then if lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then return 0 From 2d21e5e7d09c069f4c0433bf5514b06ac7a620fe Mon Sep 17 00:00:00 2001 From: tanghang97 <1769122446@qq.com> Date: Wed, 27 May 2026 09:51:31 +0800 Subject: [PATCH 2/3] fix: revert the change to the startup_config and fix the lint errors --- backend/app/gateway/app.py | 48 +++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index 08f755f0e8..c6de3f8e11 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -4,22 +4,6 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager - -def _configure_windows_event_loop_policy() -> None: - """Use an event loop policy compatible with async psycopg on Windows.""" - if sys.platform != "win32": - return - - selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) - if selector_policy is None: - return - - if not isinstance(asyncio.get_event_loop_policy(), selector_policy): - asyncio.set_event_loop_policy(selector_policy()) - - -_configure_windows_event_loop_policy() - from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -47,6 +31,22 @@ def _configure_windows_event_loop_policy() -> None: from deerflow.config import app_config as deerflow_app_config from deerflow.config.app_config import apply_logging_level + +def _configure_windows_event_loop_policy() -> None: + """Use an event loop policy compatible with async psycopg on Windows.""" + if sys.platform != "win32": + return + + selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) + if selector_policy is None: + return + + if not isinstance(asyncio.get_event_loop_policy(), selector_policy): + asyncio.set_event_loop_policy(selector_policy()) + + +_configure_windows_event_loop_policy() + AppConfig = deerflow_app_config.AppConfig get_app_config = deerflow_app_config.get_app_config @@ -178,10 +178,16 @@ async def _migrate_orphaned_threads(store, admin_user_id: str) -> int: async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Application lifespan handler.""" - # Load config and check necessary environment variables at startup + # Load config and check necessary environment variables at startup. + # `startup_config` is a local snapshot used only for one-shot bootstrap + # work (logging level, langgraph_runtime engines, channels). Request-time + # config resolution always routes through `get_app_config()` in + # `app/gateway/deps.py::get_config()` so `config.yaml` edits become + # visible without a process restart. We deliberately do NOT cache this + # snapshot on `app.state` to keep that contract enforceable. try: - app.state.config = get_app_config() - apply_logging_level(app.state.config.log_level) + startup_config = get_app_config() + apply_logging_level(startup_config.log_level) logger.info("Configuration loaded successfully") except Exception as e: error_msg = f"Failed to load configuration during gateway startup: {e}" @@ -191,7 +197,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: logger.info(f"Starting API Gateway on {config.host}:{config.port}") # Initialize LangGraph runtime components (StreamBridge, RunManager, checkpointer, store) - async with langgraph_runtime(app): + async with langgraph_runtime(app, startup_config): logger.info("LangGraph runtime initialised") # Check admin bootstrap state and migrate orphan threads after admin exists. @@ -202,7 +208,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: try: from app.channels.service import start_channel_service - channel_service = await start_channel_service(app.state.config) + channel_service = await start_channel_service(startup_config) logger.info("Channel service started: %s", channel_service.get_status()) except Exception: logger.exception("No IM channels configured or channel service failed to start") From 71b87f799faeceaae5927973e783383b1aad6351 Mon Sep 17 00:00:00 2001 From: tanghang97 <1769122446@qq.com> Date: Thu, 4 Jun 2026 09:43:02 +0800 Subject: [PATCH 3/3] fix: Address Copilot review feedback - Validate wait-for-port input and avoid PowerShell port interpolation - Require Python 3 in serve.sh launcher detection - Keep Windows event loop policy setup in sitecustomize only - Clarify sitecustomize process-wide backend behavior --- backend/app/gateway/app.py | 17 ----------------- backend/sitecustomize.py | 7 ++++++- scripts/serve.sh | 2 +- scripts/wait-for-port.sh | 14 +++++++++++++- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index c6de3f8e11..8baecb3631 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -1,6 +1,5 @@ import asyncio import logging -import sys from collections.abc import AsyncGenerator from contextlib import asynccontextmanager @@ -31,22 +30,6 @@ from deerflow.config import app_config as deerflow_app_config from deerflow.config.app_config import apply_logging_level - -def _configure_windows_event_loop_policy() -> None: - """Use an event loop policy compatible with async psycopg on Windows.""" - if sys.platform != "win32": - return - - selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) - if selector_policy is None: - return - - if not isinstance(asyncio.get_event_loop_policy(), selector_policy): - asyncio.set_event_loop_policy(selector_policy()) - - -_configure_windows_event_loop_policy() - AppConfig = deerflow_app_config.AppConfig get_app_config = deerflow_app_config.get_app_config diff --git a/backend/sitecustomize.py b/backend/sitecustomize.py index b4f2e59739..4b85819e18 100644 --- a/backend/sitecustomize.py +++ b/backend/sitecustomize.py @@ -1,4 +1,9 @@ -"""Process-wide Python startup customizations for local development.""" +"""Process-wide Python startup customizations for backend entrypoints. + +When ``backend/`` is on ``sys.path``, Python imports this module during +interpreter startup. Keep changes here suitable for all gateway, script, +migration, and test entrypoints that run in that environment. +""" from __future__ import annotations diff --git a/scripts/serve.sh b/scripts/serve.sh index bdab082ea9..1abf353ea2 100755 --- a/scripts/serve.sh +++ b/scripts/serve.sh @@ -40,7 +40,7 @@ fi _pick_python() { local candidate for candidate in python3 python py; do - if command -v "$candidate" >/dev/null 2>&1 && "$candidate" -c 'import sys' >/dev/null 2>&1; then + if command -v "$candidate" >/dev/null 2>&1 && "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info.major >= 3 else 1)' >/dev/null 2>&1; then printf '%s\n' "$candidate" return 0 fi diff --git a/scripts/wait-for-port.sh b/scripts/wait-for-port.sh index 0764d8491c..ef2522e638 100755 --- a/scripts/wait-for-port.sh +++ b/scripts/wait-for-port.sh @@ -17,12 +17,24 @@ PORT="${1:?Usage: wait-for-port.sh [timeout] [service_name]}" TIMEOUT="${2:-60}" SERVICE="${3:-Service}" +case "$PORT" in + ''|*[!0-9]*) + echo "Port must be a numeric TCP port: $PORT" >&2 + exit 1 + ;; +esac + +if [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + echo "Port must be between 1 and 65535: $PORT" >&2 + exit 1 +fi + elapsed=0 interval=1 is_port_listening() { if command -v powershell.exe >/dev/null 2>&1; then - if powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "\$ErrorActionPreference='SilentlyContinue'; if (Get-NetTCPConnection -LocalPort $PORT -State Listen) { exit 0 } else { exit 1 }" >/dev/null 2>&1; then + if WAIT_FOR_PORT_PORT="$PORT" powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "\$ErrorActionPreference='SilentlyContinue'; \$Port = [int]\$env:WAIT_FOR_PORT_PORT; if (Get-NetTCPConnection -LocalPort \$Port -State Listen) { exit 0 } else { exit 1 }" >/dev/null 2>&1; then return 0 fi fi