refactor(browser): split browser cores via build ARG, add core selector (#237)

* refactor(browser): split browser cores via build ARG, add core selector

- Replace playwright official image with ubuntu:noble base in both
  docker/Dockerfile.browser and devenv/Dockerfile.browser; install
  browsers at build time driven by ARG/ENV BROWSER_CORES
- Add GET /cores endpoint to Browser Gateway reporting available cores
- Proxy GET /browser-contexts/cores in Go handler to Browser Gateway
- Add `core` field to BrowserContextConfigModel and GatewayBrowserContext;
  context creation selects the appropriate browser instance by core
- Frontend context-setting page fetches available cores and renders a
  core selector; saves core as part of the config JSON
- install.sh prompts for browser core selection and writes BROWSER_CORES
  to .env; builds the browser image locally before docker compose up
- Regenerate OpenAPI spec and TypeScript SDK

* fix: lint
This commit is contained in:
Acbox Liu
2026-03-14 12:37:20 +08:00
committed by GitHub
parent 627b673a5c
commit c8728ffc2c
22 changed files with 364 additions and 33 deletions
+35 -6
View File
@@ -1,7 +1,36 @@
import { chromium } from 'playwright'
import { chromium, firefox } from 'playwright'
import type { Browser } from 'playwright'
export const initBrowser = async () => {
return await chromium.launch({
headless: true,
})
}
export type BrowserCore = 'chromium' | 'firefox'
export const browsers = new Map<BrowserCore, Browser>()
export const initBrowsers = async (): Promise<Map<BrowserCore, Browser>> => {
const raw = process.env.BROWSER_CORES ?? 'chromium'
const cores = raw.split(',').map(s => s.trim()) as BrowserCore[]
for (const core of cores) {
if (core === 'chromium') {
browsers.set('chromium', await chromium.launch({ headless: true }))
} else if (core === 'firefox') {
browsers.set('firefox', await firefox.launch({ headless: true }))
}
}
if (browsers.size === 0) {
browsers.set('chromium', await chromium.launch({ headless: true }))
}
return browsers
}
export const getBrowser = (core: BrowserCore = 'chromium'): Browser => {
const b = browsers.get(core) ?? browsers.values().next().value
if (!b) throw new Error(`Browser core "${core}" is not available`)
return b
}
export const getAvailableCores = (): BrowserCore[] => {
const raw = process.env.BROWSER_CORES ?? 'chromium'
return raw.split(',').map(s => s.trim()) as BrowserCore[]
}
+9 -4
View File
@@ -2,13 +2,16 @@ import { Elysia } from 'elysia'
import { loadConfig } from '@memoh/config'
import { corsMiddleware } from './middlewares/cors'
import { errorMiddleware } from './middlewares/error'
import { initBrowser } from './browser'
import { initBrowsers, browsers } from './browser'
import { contextModule } from './modules/context'
import { devicesModule } from './modules/devices'
import { coresModule } from './modules/cores'
const config = loadConfig('../../config.toml')
export const browser = await initBrowser()
await initBrowsers()
export { browsers }
const app = new Elysia()
.use(corsMiddleware)
@@ -16,10 +19,13 @@ const app = new Elysia()
.get('/health', () => ({
status: 'ok',
}))
.use(coresModule)
.use(contextModule)
.use(devicesModule)
.onStop(async () => {
await browser.close()
for (const browser of browsers.values()) {
await browser.close()
}
})
.listen({
port: config.browser_gateway.port ?? 8083,
@@ -28,4 +34,3 @@ const app = new Elysia()
})
console.log(`🌐 Browser Gateway is running at ${app.server!.url}`)
+1
View File
@@ -1,6 +1,7 @@
import { z } from 'zod'
export const BrowserContextConfigModel = z.object({
core: z.enum(['chromium', 'firefox']).optional().default('chromium'),
viewport: z.object({
width: z.number(),
height: z.number(),
+6 -4
View File
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia'
import { storage } from '../storage'
import { z } from 'zod'
import { BrowserContextConfigModel } from '../models'
import { browser } from '..'
import { getBrowser } from '../browser'
import { actionModule } from './action'
export const contextModule = new Elysia({ prefix: '/context' })
@@ -14,7 +14,7 @@ export const contextModule = new Elysia({ prefix: '/context' })
const { id } = query
const entry = storage.get(id)
if (!entry) return null
return { id: entry.id, name: entry.name, config: entry.config }
return { id: entry.id, name: entry.name, core: entry.core, config: entry.config }
}, {
query: z.object({
id: z.string(),
@@ -24,6 +24,8 @@ export const contextModule = new Elysia({ prefix: '/context' })
'/',
async ({ body }) => {
const { name, config, id } = body
const core = config.core ?? 'chromium'
const browser = getBrowser(core)
const context = await browser.newContext({
viewport: config.viewport,
userAgent: config.userAgent,
@@ -37,8 +39,8 @@ export const contextModule = new Elysia({ prefix: '/context' })
ignoreHTTPSErrors: config.ignoreHTTPSErrors,
proxy: config.proxy,
})
storage.set(id, { id, name, context, config })
return { id, name, config }
storage.set(id, { id, name, core, context, config })
return { id, name, core, config }
},
{
body: z.object({
+7
View File
@@ -0,0 +1,7 @@
import { Elysia } from 'elysia'
import { getAvailableCores } from '../browser'
export const coresModule = new Elysia({ prefix: '/cores' })
.get('/', () => {
return { cores: getAvailableCores() }
})
+2
View File
@@ -1,9 +1,11 @@
import type { BrowserContext, Page } from 'playwright'
import type { BrowserContextConfig } from '../models'
import type { BrowserCore } from '../browser'
export interface GatewayBrowserContext {
id: string
name: string
core: BrowserCore
context: BrowserContext
config: BrowserContextConfig
activePage?: Page