param( [string]$MysqlDatabaseUrl = $env:MYSQL_DATABASE_URL, [string]$MysqlPipPackages = $(if ($env:MYSQL_PIP_PACKAGES) { $env:MYSQL_PIP_PACKAGES } else { "asyncmy==0.2.10" }), [string]$CreateDatabase = $(if ($env:CREATE_DATABASE) { $env:CREATE_DATABASE } else { "1" }), [string]$DropEmptyTargetSchema = $(if ($env:DROP_EMPTY_TARGET_SCHEMA) { $env:DROP_EMPTY_TARGET_SCHEMA } else { "1" }), [string]$TruncateTarget = $(if ($env:TRUNCATE_TARGET) { $env:TRUNCATE_TARGET } else { "0" }), [string]$SqliteDb = $(if ($env:SQLITE_DB) { $env:SQLITE_DB } else { "" }), [string]$ConfigFile = $(if ($env:DEER_FLOW_CONFIG_PATH) { $env:DEER_FLOW_CONFIG_PATH } else { "" }), [string]$EnvFile = $(if ($env:ENV_FILE) { $env:ENV_FILE } else { "" }), [string]$PythonExe = $(if ($env:PYTHON_EXE) { $env:PYTHON_EXE } else { "" }) ) $ErrorActionPreference = "Stop" function Die([string]$Message) { Write-Error $Message exit 1 } function Require-Command([string]$Name) { if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { Die "command not found: $Name" } } function Require-File([string]$Path) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { Die "required file not found: $Path" } } function Require-Dir([string]$Path) { if (-not (Test-Path -LiteralPath $Path -PathType Container)) { Die "required directory not found: $Path" } } function Resolve-ExistingFile([string]$Path) { return (Resolve-Path -LiteralPath $Path).Path } function Get-EnvFileValue([string]$Path, [string]$Key) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return "" } $line = Get-Content -LiteralPath $Path | Where-Object { $_ -match "^\s*$([regex]::Escape($Key))=" } | Select-Object -Last 1 if (-not $line) { return "" } return ($line -replace "^\s*$([regex]::Escape($Key))=", "") } function Upsert-EnvFileValue([string]$Path, [string]$Key, [string]$Value) { $existing = @() if (Test-Path -LiteralPath $Path -PathType Leaf) { $existing = Get-Content -LiteralPath $Path | Where-Object { $_ -notmatch "^\s*$([regex]::Escape($Key))=" } } $existing + "$Key=$Value" | Set-Content -LiteralPath $Path -Encoding UTF8 } function Is-Enabled([string]$Value) { return @("1", "true", "yes", "on") -contains $Value.ToLowerInvariant() } function Run-Native([string]$Command, [string[]]$Arguments) { & $Command @Arguments if ($LASTEXITCODE -ne 0) { Die "$Command failed with exit code $LASTEXITCODE" } } $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $BackendDir = Join-Path $ScriptDir "backend" $DefaultDeerFlowHome = Join-Path $BackendDir ".deer-flow" if (-not $ConfigFile) { $ConfigFile = Join-Path $ScriptDir "config.yaml" } if (-not $EnvFile) { $EnvFile = Join-Path $ScriptDir ".env" } if (-not $SqliteDb) { $SqliteDb = Join-Path $DefaultDeerFlowHome "data\deerflow.db" } if ($PythonExe) { Require-File $PythonExe $PythonExe = Resolve-ExistingFile $PythonExe } else { Require-Command uv } Require-File $EnvFile Require-File $ConfigFile Require-File $SqliteDb Require-File (Join-Path $BackendDir "pyproject.toml") Require-File (Join-Path $BackendDir "scripts\migrate_business_sqlite_to_mysql.py") Require-File (Join-Path $BackendDir "scripts\update_database_config.py") Require-File (Join-Path $ScriptDir "extensions_config.json") Require-File (Join-Path $ScriptDir "skill_evolution.json") Require-Dir (Join-Path $ScriptDir "skills") if (-not $MysqlDatabaseUrl) { $MysqlDatabaseUrl = Get-EnvFileValue $EnvFile "MYSQL_DATABASE_URL" } if (-not $MysqlDatabaseUrl) { Die "MYSQL_DATABASE_URL is required. Example: .\switch_database_to_mysql.ps1 -MysqlDatabaseUrl 'mysql+asyncmy://user:pass@127.0.0.1:3306/deerflow?charset=utf8mb4'" } $ConfigFile = Resolve-ExistingFile $ConfigFile $EnvFile = Resolve-ExistingFile $EnvFile $SqliteDb = Resolve-ExistingFile $SqliteDb $DeerFlowHome = if ($env:DEER_FLOW_HOME) { $env:DEER_FLOW_HOME } else { $DefaultDeerFlowHome } New-Item -ItemType Directory -Force -Path $DeerFlowHome | Out-Null $DeerFlowHome = (Resolve-Path -LiteralPath $DeerFlowHome).Path $BackupRoot = Join-Path $DeerFlowHome "backups" New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null $Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $BackupDir = Join-Path $BackupRoot "mysql-switch-$Timestamp" New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null Copy-Item -LiteralPath $ConfigFile -Destination (Join-Path $BackupDir "config.yaml.bak") -Force Copy-Item -LiteralPath $EnvFile -Destination (Join-Path $BackupDir "env.bak") -Force Copy-Item -LiteralPath $SqliteDb -Destination (Join-Path $BackupDir "deerflow.db.bak") -Force $env:MYSQL_DATABASE_URL = $MysqlDatabaseUrl $env:DEER_FLOW_PROJECT_ROOT = $ScriptDir $env:DEER_FLOW_HOME = $DeerFlowHome $env:DEER_FLOW_CONFIG_PATH = $ConfigFile $env:DEER_FLOW_EXTENSIONS_CONFIG_PATH = Join-Path $ScriptDir "extensions_config.json" $env:DEER_FLOW_SKILL_EVOLUTION_CONFIG_PATH = Join-Path $ScriptDir "skill_evolution.json" $env:DEER_FLOW_SKILLS_PATH = Join-Path $ScriptDir "skills" $PythonPathParts = @( $BackendDir, (Join-Path $BackendDir "packages\harness") ) if ($env:PYTHONPATH) { $PythonPathParts += $env:PYTHONPATH } $env:PYTHONPATH = ($PythonPathParts -join [IO.Path]::PathSeparator) Push-Location $BackendDir try { $packages = @($MysqlPipPackages -split "\s+" | Where-Object { $_ }) if ($PythonExe) { if ($packages.Count -gt 0) { Write-Host "Installing MySQL driver into selected Python environment: $MysqlPipPackages" Run-Native $PythonExe (@("-m", "pip", "install") + $packages) } Run-Native $PythonExe @("-c", "import asyncmy") } else { if (-not (Test-Path -LiteralPath (Join-Path $BackendDir ".venv") -PathType Container)) { Write-Host "Creating local uv virtual environment..." Run-Native "uv" @("venv") } if ($packages.Count -gt 0) { Write-Host "Installing MySQL driver into local backend environment: $MysqlPipPackages" Run-Native "uv" (@("pip", "install") + $packages) } Run-Native "uv" @("run", "--no-sync", "python", "-c", "import asyncmy") } $migrationArgs = @( "scripts\migrate_business_sqlite_to_mysql.py", "--sqlite", $SqliteDb, "--mysql-url", $MysqlDatabaseUrl ) if (Is-Enabled $CreateDatabase) { $migrationArgs += "--create-database" } if (Is-Enabled $DropEmptyTargetSchema) { $migrationArgs += "--drop-empty-target-schema" } if (Is-Enabled $TruncateTarget) { $migrationArgs += "--truncate-target" } Write-Host "Backups written to: $BackupDir" Write-Host "Migrating business tables to MySQL..." if ($PythonExe) { Run-Native $PythonExe $migrationArgs } else { Run-Native "uv" (@("run", "--no-sync", "python") + $migrationArgs) } $configArgs = @( "scripts\update_database_config.py", "--config", $ConfigFile, "--mysql-url-env", "MYSQL_DATABASE_URL", "--checkpointer-sqlite", $SqliteDb ) if ($PythonExe) { Run-Native $PythonExe $configArgs } else { Run-Native "uv" (@("run", "--no-sync", "python") + $configArgs) } Upsert-EnvFileValue $EnvFile "MYSQL_DATABASE_URL" $MysqlDatabaseUrl } finally { Pop-Location } Write-Host "" Write-Host "MySQL switch completed." Write-Host " Config updated: $ConfigFile" Write-Host " Env updated: $EnvFile" Write-Host " Backup dir: $BackupDir" Write-Host "" Write-Host "Start the local backend from backend directory, for example:" Write-Host " cd backend" Write-Host " `$env:PYTHONPATH='.'" Write-Host " uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001"