diff --git a/browser-use-service/main.py b/browser-use-service/main.py index af098c5..6ad51c9 100644 --- a/browser-use-service/main.py +++ b/browser-use-service/main.py @@ -72,6 +72,54 @@ def detect_browser_from_profile(profile_path: str) -> str | None: return None +def find_firefox_profile() -> str | None: + """Auto-detect Firefox profile directory cross-platform.""" + import sys + home = os.path.expanduser("~") + candidates = [] + + if sys.platform == "win32": + appdata = os.environ.get("APPDATA", "") + if appdata: + profiles_dir = os.path.join(appdata, "Mozilla", "Firefox", "Profiles") + else: + profiles_dir = os.path.join(home, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") + elif sys.platform == "darwin": + profiles_dir = os.path.join(home, "Library", "Application Support", "Firefox", "Profiles") + else: + profiles_dir = os.path.join(home, ".mozilla", "firefox") + + if os.path.isdir(profiles_dir): + for entry in os.listdir(profiles_dir): + full = os.path.join(profiles_dir, entry) + if os.path.isdir(full) and (".default" in entry.lower() or ".dev-edition" in entry.lower()): + candidates.append(full) + candidates.sort(key=lambda p: "default-release" in p.lower(), reverse=True) + if candidates: + logger.info("Auto-detected Firefox profile: %s", candidates[0]) + return candidates[0] + return None + + +def find_chrome_profile() -> str | None: + """Auto-detect Chrome/Chromium profile directory cross-platform.""" + import sys + home = os.path.expanduser("~") + + if sys.platform == "win32": + base = os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "User Data") + elif sys.platform == "darwin": + base = os.path.join(home, "Library", "Application Support", "Google", "Chrome") + else: + base = os.path.join(home, ".config", "google-chrome") + + default_profile = os.path.join(base, "Default") + if os.path.isdir(default_profile): + logger.info("Auto-detected Chrome profile: %s", default_profile) + return default_profile + return None + + def copy_firefox_profile(src_path: str) -> str: """Copy essential Firefox profile files to a temp dir for Playwright.""" essential = ['cookies.sqlite', 'webappsstore.sqlite', 'permissions.sqlite'] @@ -745,12 +793,27 @@ def cleanup_chrome(): async def scrape_facebook(profile_path: str | None = None, force: bool = False) -> dict: """Dispatcher — Firefox primary, browser-use Agent fallback.""" effective_path = profile_path or FX_PROFILE + + # Auto-detect Firefox profile if none provided + if not effective_path: + detected = find_firefox_profile() + if detected: + effective_path = detected + os.environ["FX_PROFILE"] = detected + browser_type = detect_browser_from_profile(effective_path) if not browser_type and CHROME_PROFILE: browser_type = detect_browser_from_profile(CHROME_PROFILE) effective_path = CHROME_PROFILE + # Auto-detect Chrome profile if still nothing + if not browser_type: + detected_chrome = find_chrome_profile() + if detected_chrome: + effective_path = detected_chrome + browser_type = 'chromium' + logger.info("Detected browser: %s (profile: %s)", browser_type or "none", effective_path) # Firefox primary (raw Playwright, stealth) diff --git a/browser-use-service/requirements.txt b/browser-use-service/requirements.txt new file mode 100644 index 0000000..1d64ec7 --- /dev/null +++ b/browser-use-service/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.115.0 +uvicorn>=0.34.0 +playwright>=1.49.0 +browser-use>=0.1.0 +langchain-ollama>=0.2.0 diff --git a/package.json b/package.json index 2e0a228..562ee96 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "scripts": { "dev": "npm run dev:precheck & npm run dev:ollama & npm run dev:start", "dev:signaling": "node signaling-server.mjs", - "dev:open": "powershell -NoProfile -Command \"Start-Sleep 8; Start-Process 'http://localhost:3001/splash'\"", + "dev:open": "node scripts/open-browser.mjs", "dev:start": "concurrently -n AI,BROWSE,SIGNAL,NEXT,OPEN -c cyan,magenta,yellow,green,white \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\" \"npm run dev:open\"", "dev:next": "next dev -p 3006", - "dev:precheck": "powershell -NoProfile -Command \"Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3001,3006,3007,3008 } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue; Write-Host ('Freed port '+$_.LocalPort) }\"", - "dev:ollama": "powershell -NoProfile -Command \"$ollama = if (Get-Command ollama -ErrorAction SilentlyContinue) { 'ollama' } else { Join-Path $env:LOCALAPPDATA 'Programs\\Ollama\\ollama.exe' }; if (-not (Get-Process ollama -ErrorAction SilentlyContinue)) { Start-Process $ollama -ArgumentList 'serve' -WindowStyle Hidden; Start-Sleep 3 }; exit 0\"", + "dev:precheck": "node scripts/precheck.mjs", + "dev:ollama": "node scripts/ensure-ollama.mjs", "dev:rust": "node ai-server/index.mjs", - "dev:browser-use": "set FX_PROFILE=C:\\Users\\USER-PC\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\h8p11vlj.default-release && cd browser-use-service && python main.py", + "dev:browser-use": "cd browser-use-service && node ../scripts/run-python.mjs main.py", + "setup": "node scripts/setup.mjs", "build": "next build", "start": "next start -p 3006", "lint": "eslint" diff --git a/scripts/ensure-ollama.mjs b/scripts/ensure-ollama.mjs new file mode 100644 index 0000000..8743779 --- /dev/null +++ b/scripts/ensure-ollama.mjs @@ -0,0 +1,47 @@ +import { execSync, spawn } from "node:child_process" +import { platform } from "node:os" + +function isRunning() { + try { + if (platform() === "win32") { + execSync('tasklist /FI "IMAGENAME eq ollama.exe" 2>nul | findstr ollama', { stdio: "pipe", timeout: 3000 }) + } else { + execSync('pgrep -x ollama', { stdio: "pipe", timeout: 3000 }) + } + return true + } catch { + return false + } +} + +function findOllama() { + if (platform() === "win32") { + try { + return execSync("where ollama", { encoding: "utf8", timeout: 3000 }).trim().split("\n")[0] + } catch {} + const local = `${process.env.LOCALAPPDATA}\\Programs\\Ollama\\ollama.exe` + const programFiles = `${process.env.PROGRAMFILES}\\Ollama\\ollama.exe` + if (local) try { execSync(`"${local}" --version`, { stdio: "ignore", timeout: 2000 }); return local } catch {} + if (programFiles) try { execSync(`"${programFiles}" --version`, { stdio: "ignore", timeout: 2000 }); return programFiles } catch {} + } else { + try { return execSync("which ollama", { encoding: "utf8", timeout: 3000 }).trim() } catch {} + } + return null +} + +if (!isRunning()) { + const bin = findOllama() + if (!bin) { + console.error("Ollama not found. Install from https://ollama.com") + process.exit(1) + } + console.log("Starting Ollama...") + if (platform() === "win32") { + spawn(bin, ["serve"], { stdio: "ignore", detached: true, windowsHide: true }).unref() + } else { + spawn(bin, ["serve"], { stdio: "ignore", detached: true }).unref() + } + execSync("sleep 3", { stdio: "ignore", timeout: 5000 }) +} else { + console.log("Ollama already running") +} diff --git a/scripts/open-browser.mjs b/scripts/open-browser.mjs new file mode 100644 index 0000000..af8622f --- /dev/null +++ b/scripts/open-browser.mjs @@ -0,0 +1,23 @@ +import { execSync } from "node:child_process" +import { platform } from "node:os" + +const url = process.argv[2] || "http://localhost:3001/splash" + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +async function main() { + await sleep(8000) + try { + if (platform() === "win32") { + execSync(`start "" "${url}"`, { stdio: "ignore", timeout: 5000 }) + } else if (platform() === "darwin") { + execSync(`open "${url}"`, { stdio: "ignore", timeout: 5000 }) + } else { + execSync(`xdg-open "${url}"`, { stdio: "ignore", timeout: 5000 }) + } + } catch (e) { + console.error("Failed to open browser:", e.message) + } +} + +main() diff --git a/scripts/precheck.mjs b/scripts/precheck.mjs new file mode 100644 index 0000000..30f471f --- /dev/null +++ b/scripts/precheck.mjs @@ -0,0 +1,30 @@ +import { execSync } from "node:child_process" +import { platform } from "node:os" + +const PORTS = [3001, 3006, 3007, 3008] + +if (platform() === "win32") { + for (const port of PORTS) { + try { + const out = execSync(`netstat -ano | findstr "LISTENING" | findstr ":${port} "`, { encoding: "utf8", timeout: 5000 }) + const lines = out.trim().split("\n").filter(Boolean) + for (const line of lines) { + const parts = line.trim().split(/\s+/) + const pid = parts[parts.length - 1] + if (pid) { + try { execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore", timeout: 3000 }); console.log(`Freed port ${port} (PID ${pid})`) } catch {} + } + } + } catch {} + } +} else { + for (const port of PORTS) { + try { + const pid = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: "utf8", timeout: 5000 }).trim() + if (pid) { + execSync(`kill -9 ${pid}`, { stdio: "ignore", timeout: 3000 }) + console.log(`Freed port ${port} (PID ${pid})`) + } + } catch {} + } +} diff --git a/scripts/run-python.mjs b/scripts/run-python.mjs new file mode 100644 index 0000000..d3a3134 --- /dev/null +++ b/scripts/run-python.mjs @@ -0,0 +1,27 @@ +import { execSync, spawn } from "node:child_process" +import { platform } from "node:os" + +function detectPython() { + const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"] + for (const cmd of candidates) { + try { + const out = execSync(platform() === "win32" ? `where ${cmd}` : `which ${cmd}`, { encoding: "utf8", timeout: 5000 }) + const path = out.trim().split("\n")[0].replace(/\r$/, "") + if (path) return path + } catch {} + } + console.error("Python not found. Install Python 3 from https://python.org") + process.exit(1) +} + +const PYTHON = detectPython() +const script = process.argv[2] +const args = process.argv.slice(3) + +if (!script) { + console.error("Usage: node run-python.mjs