Skip to content

Commit 44bd66f

Browse files
Merge branch 'main' into fix/feishu-webhook-cache-stream-regressions
2 parents 3068a1e + e7844df commit 44bd66f

165 files changed

Lines changed: 16377 additions & 5581 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
strategy:
3737
fail-fast: false
3838
matrix:
39-
image: [server, agent, web, mcp, browser]
39+
image: [server, agent, web, mcp, browser, sparse]
4040
platform: [linux/amd64, linux/arm64]
4141
include:
4242
- image: server
@@ -49,6 +49,8 @@ jobs:
4949
dockerfile: docker/Dockerfile.mcp
5050
- image: browser
5151
dockerfile: docker/Dockerfile.browser
52+
- image: sparse
53+
dockerfile: docker/Dockerfile.sparse
5254
- platform: linux/amd64
5355
runner: ubuntu-latest
5456
- platform: linux/arm64
@@ -132,7 +134,7 @@ jobs:
132134
needs: build
133135
strategy:
134136
matrix:
135-
image: [server, agent, web, mcp, browser]
137+
image: [server, agent, web, mcp, browser, sparse]
136138
steps:
137139
- name: Download digests
138140
uses: actions/download-artifact@v4

DEPLOYMENT.md

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,64 @@ git clone https://github.com/memohai/Memoh.git
1515
cd Memoh
1616
cp conf/app.docker.toml config.toml
1717
nano config.toml # Change passwords and JWT secret
18-
sudo docker compose up -d
1918
```
2019

2120
> On macOS or if your user is in the `docker` group, `sudo` is not required.
2221
2322
> **Important**: You must create `config.toml` before starting. `docker-compose.yml` mounts `./config.toml` into the containers — running without it will fail.
2423
24+
### Standard startup (with Qdrant + Browser)
25+
26+
```bash
27+
sudo docker compose --profile qdrant --profile browser up -d
28+
```
29+
30+
### Minimal startup (core only)
31+
32+
```bash
33+
sudo docker compose up -d
34+
```
35+
2536
Access:
2637
- Web UI: http://localhost:8082
2738
- API: http://localhost:8080
2839
- Agent: http://localhost:8081
2940

3041
Default credentials: `admin` / `admin123` (change in `config.toml`)
3142

43+
## Docker Compose Profiles
44+
45+
The base `docker-compose.yml` contains all services. Core services (postgres, server, agent, web) always start. Optional services are gated by profiles and only start when explicitly enabled:
46+
47+
| Profile | Service | Description |
48+
|---------|---------|-------------|
49+
| `qdrant` | Qdrant | Vector database for memory semantic search |
50+
| `browser` | Browser | Browser automation gateway (Playwright) |
51+
| `openviking` | OpenViking | Self-hosted OpenViking memory provider |
52+
53+
### Supported combinations
54+
55+
```bash
56+
# Core + Qdrant + Browser (recommended default)
57+
docker compose --profile qdrant --profile browser up -d
58+
59+
# Core + Qdrant + OpenViking (self-hosted)
60+
docker compose --profile qdrant --profile openviking up -d
61+
```
62+
63+
### SaaS / external providers
64+
65+
For Mem0 or OpenViking SaaS, no profile is needed. Configure the provider directly in the Memoh admin UI with the external `base_url` and API key.
66+
67+
### China Mainland Mirror
68+
69+
Uncomment `registry = "memoh.cn"` in `config.toml` under `[mcp]`, then add the CN overlay:
70+
71+
```bash
72+
sudo docker compose -f docker-compose.yml -f docker/docker-compose.cn.yml \
73+
--profile qdrant --profile browser up -d
74+
```
75+
3276
## Prerequisites
3377

3478
- Docker (with Docker Compose v2)
@@ -43,14 +87,6 @@ Recommended changes for production:
4387
- `auth.jwt_secret` — JWT secret (generate with `openssl rand -base64 32`)
4488
- `postgres.password` — Database password (also set `POSTGRES_PASSWORD` env var)
4589

46-
### China Mainland Mirror
47-
48-
Uncomment `registry = "memoh.cn"` in `config.toml` under `[mcp]`, then use:
49-
50-
```bash
51-
sudo docker compose -f docker-compose.yml -f docker/docker-compose.cn.yml up -d
52-
```
53-
5490
## Common Commands
5591

5692
> Prefix with `sudo` on Linux if your user is not in the `docker` group.

apps/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@memoh/agent-gateway",
3-
"version": "0.4.3",
3+
"version": "0.5.0-beta.1",
44
"scripts": {
55
"dev": "bun run --watch src/index.ts",
66
"build": "bun build src/index.ts --outfile dist/index.js --target bun --minify",

apps/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@memoh/browser-gateway",
33
"type": "module",
4-
"version": "0.4.3",
4+
"version": "0.5.0-beta.1",
55
"scripts": {
66
"dev": "bun run --watch src/index.ts",
77
"build": "bun build src/index.ts --outfile dist/index.js --target bun --minify --external playwright --external playwright-core",

apps/browser/src/browser.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
1-
import { chromium } from 'playwright'
1+
import { chromium, firefox } from 'playwright'
2+
import type { Browser } from 'playwright'
23

3-
export const initBrowser = async () => {
4-
return await chromium.launch({
5-
headless: true,
6-
})
7-
}
4+
export type BrowserCore = 'chromium' | 'firefox'
5+
6+
export const browsers = new Map<BrowserCore, Browser>()
7+
8+
export const initBrowsers = async (): Promise<Map<BrowserCore, Browser>> => {
9+
const raw = process.env.BROWSER_CORES ?? 'chromium'
10+
const cores = raw.split(',').map(s => s.trim()) as BrowserCore[]
11+
12+
for (const core of cores) {
13+
if (core === 'chromium') {
14+
browsers.set('chromium', await chromium.launch({ headless: true }))
15+
} else if (core === 'firefox') {
16+
browsers.set('firefox', await firefox.launch({ headless: true }))
17+
}
18+
}
19+
20+
if (browsers.size === 0) {
21+
browsers.set('chromium', await chromium.launch({ headless: true }))
22+
}
23+
24+
return browsers
25+
}
26+
27+
export const getBrowser = (core: BrowserCore = 'chromium'): Browser => {
28+
const b = browsers.get(core) ?? browsers.values().next().value
29+
if (!b) throw new Error(`Browser core "${core}" is not available`)
30+
return b
31+
}
32+
33+
export const getAvailableCores = (): BrowserCore[] => {
34+
const raw = process.env.BROWSER_CORES ?? 'chromium'
35+
return raw.split(',').map(s => s.trim()) as BrowserCore[]
36+
}

apps/browser/src/index.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,30 @@ import { Elysia } from 'elysia'
22
import { loadConfig } from '@memoh/config'
33
import { corsMiddleware } from './middlewares/cors'
44
import { errorMiddleware } from './middlewares/error'
5-
import { initBrowser } from './browser'
5+
import { initBrowsers, browsers } from './browser'
66
import { contextModule } from './modules/context'
77
import { devicesModule } from './modules/devices'
8+
import { coresModule } from './modules/cores'
89

910
const config = loadConfig('../../config.toml')
1011

11-
export const browser = await initBrowser()
12+
await initBrowsers()
13+
14+
export { browsers }
1215

1316
const app = new Elysia()
1417
.use(corsMiddleware)
1518
.use(errorMiddleware)
1619
.get('/health', () => ({
1720
status: 'ok',
1821
}))
22+
.use(coresModule)
1923
.use(contextModule)
2024
.use(devicesModule)
2125
.onStop(async () => {
22-
await browser.close()
26+
for (const browser of browsers.values()) {
27+
await browser.close()
28+
}
2329
})
2430
.listen({
2531
port: config.browser_gateway.port ?? 8083,
@@ -28,4 +34,3 @@ const app = new Elysia()
2834
})
2935

3036
console.log(`🌐 Browser Gateway is running at ${app.server!.url}`)
31-

apps/browser/src/models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod'
22

33
export const BrowserContextConfigModel = z.object({
4+
core: z.enum(['chromium', 'firefox']).optional().default('chromium'),
45
viewport: z.object({
56
width: z.number(),
67
height: z.number(),

apps/browser/src/modules/context.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia'
22
import { storage } from '../storage'
33
import { z } from 'zod'
44
import { BrowserContextConfigModel } from '../models'
5-
import { browser } from '..'
5+
import { getBrowser } from '../browser'
66
import { actionModule } from './action'
77

88
export const contextModule = new Elysia({ prefix: '/context' })
@@ -14,7 +14,7 @@ export const contextModule = new Elysia({ prefix: '/context' })
1414
const { id } = query
1515
const entry = storage.get(id)
1616
if (!entry) return null
17-
return { id: entry.id, name: entry.name, config: entry.config }
17+
return { id: entry.id, name: entry.name, core: entry.core, config: entry.config }
1818
}, {
1919
query: z.object({
2020
id: z.string(),
@@ -24,6 +24,8 @@ export const contextModule = new Elysia({ prefix: '/context' })
2424
'/',
2525
async ({ body }) => {
2626
const { name, config, id } = body
27+
const core = config.core ?? 'chromium'
28+
const browser = getBrowser(core)
2729
const context = await browser.newContext({
2830
viewport: config.viewport,
2931
userAgent: config.userAgent,
@@ -37,8 +39,8 @@ export const contextModule = new Elysia({ prefix: '/context' })
3739
ignoreHTTPSErrors: config.ignoreHTTPSErrors,
3840
proxy: config.proxy,
3941
})
40-
storage.set(id, { id, name, context, config })
41-
return { id, name, config }
42+
storage.set(id, { id, name, core, context, config })
43+
return { id, name, core, config }
4244
},
4345
{
4446
body: z.object({

apps/browser/src/modules/cores.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Elysia } from 'elysia'
2+
import { getAvailableCores } from '../browser'
3+
4+
export const coresModule = new Elysia({ prefix: '/cores' })
5+
.get('/', () => {
6+
return { cores: getAvailableCores() }
7+
})

apps/browser/src/types/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { BrowserContext, Page } from 'playwright'
22
import type { BrowserContextConfig } from '../models'
3+
import type { BrowserCore } from '../browser'
34

45
export interface GatewayBrowserContext {
56
id: string
67
name: string
8+
core: BrowserCore
79
context: BrowserContext
810
config: BrowserContextConfig
911
activePage?: Page

0 commit comments

Comments
 (0)