mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
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:
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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,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(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Elysia } from 'elysia'
|
||||
import { getAvailableCores } from '../browser'
|
||||
|
||||
export const coresModule = new Elysia({ prefix: '/cores' })
|
||||
.get('/', () => {
|
||||
return { cores: getAvailableCores() }
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -393,6 +393,9 @@
|
||||
"timezoneId": "Timezone",
|
||||
"timezonePlaceholder": "e.g. America/New_York",
|
||||
"ignoreHTTPSErrors": "Ignore HTTPS Errors",
|
||||
"core": "Browser Core",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"config": "Configuration"
|
||||
},
|
||||
"mcp": {
|
||||
|
||||
@@ -389,6 +389,9 @@
|
||||
"timezoneId": "时区",
|
||||
"timezonePlaceholder": "例如 Asia/Shanghai",
|
||||
"ignoreHTTPSErrors": "忽略 HTTPS 错误",
|
||||
"core": "浏览器内核",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"config": "配置"
|
||||
},
|
||||
"mcp": {
|
||||
|
||||
@@ -41,6 +41,31 @@
|
||||
{{ $t('browserContext.config') }}
|
||||
</h3>
|
||||
|
||||
<FormField
|
||||
v-slot="{ value, handleChange }"
|
||||
name="core"
|
||||
>
|
||||
<FormItem>
|
||||
<Label>{{ $t('browserContext.core') }}</Label>
|
||||
<FormControl>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
v-for="c in availableCores"
|
||||
:key="c"
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm transition-colors"
|
||||
:class="value === c
|
||||
? 'border-primary bg-primary/10 text-primary font-medium'
|
||||
: 'border-border bg-card text-muted-foreground hover:bg-accent'"
|
||||
@click="handleChange(c)"
|
||||
>
|
||||
{{ $t(`browserContext.${c}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
@@ -212,10 +237,11 @@ import {
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
||||
import { putBrowserContextsById, deleteBrowserContextsById } from '@memoh/sdk'
|
||||
import { getBrowserContextsCoresQuery } from '@memoh/sdk/colada'
|
||||
import type { BrowsercontextsBrowserContext } from '@memoh/sdk'
|
||||
import { inject, watch, type Ref } from 'vue'
|
||||
import { inject, watch, computed, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useDialogMutation } from '@/composables/useDialogMutation'
|
||||
@@ -228,7 +254,11 @@ const queryCache = useQueryCache()
|
||||
|
||||
const curContext = inject<Ref<BrowsercontextsBrowserContext | undefined>>('curBrowserContext')
|
||||
|
||||
const { data: coresData } = useQuery(getBrowserContextsCoresQuery())
|
||||
const availableCores = computed(() => coresData.value?.cores ?? ['chromium'])
|
||||
|
||||
interface ConfigShape {
|
||||
core?: string
|
||||
viewport?: { width?: number; height?: number }
|
||||
userAgent?: string
|
||||
deviceScaleFactor?: number
|
||||
@@ -251,6 +281,7 @@ function parseConfig(ctx: BrowsercontextsBrowserContext | undefined): ConfigShap
|
||||
|
||||
const schema = toTypedSchema(z.object({
|
||||
name: z.string().min(1),
|
||||
core: z.enum(['chromium', 'firefox']).optional(),
|
||||
viewportWidth: z.coerce.number().optional(),
|
||||
viewportHeight: z.coerce.number().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
@@ -269,6 +300,7 @@ watch(() => curContext?.value, (ctx) => {
|
||||
form.resetForm({
|
||||
values: {
|
||||
name: ctx.name || '',
|
||||
core: (cfg.core as 'chromium' | 'firefox') ?? 'chromium',
|
||||
viewportWidth: cfg.viewport?.width ?? 1280,
|
||||
viewportHeight: cfg.viewport?.height ?? 720,
|
||||
userAgent: cfg.userAgent ?? '',
|
||||
@@ -307,7 +339,9 @@ const handleSave = form.handleSubmit(async (values) => {
|
||||
const id = curContext?.value?.id
|
||||
if (!id) return
|
||||
|
||||
const config: Record<string, any> = {}
|
||||
const config: Record<string, any> = {
|
||||
core: values.core ?? 'chromium',
|
||||
}
|
||||
if (values.viewportWidth || values.viewportHeight) {
|
||||
config.viewport = {
|
||||
width: values.viewportWidth || 1280,
|
||||
|
||||
Reference in New Issue
Block a user