feat: rebuild as multi-user web agent

This commit is contained in:
wuyang
2026-07-26 06:52:12 +08:00
parent 45792c8fd5
commit 37f83eaac7
634 changed files with 5060 additions and 139619 deletions
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN python -m pip install --no-cache-dir --upgrade pip==26.1.2
COPY --chown=65534:65534 --chmod=0444 e2e/stub_provider.py ./stub_provider.py
USER 65534:65534
EXPOSE 9000
HEALTHCHECK --interval=2s --timeout=2s --retries=20 CMD \
python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/health', timeout=1)"
CMD ["python", "/app/stub_provider.py"]
+24
View File
@@ -0,0 +1,24 @@
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update \
&& apt-get install --yes --no-install-recommends openssh-client \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml README.md ./
COPY agent_platform ./agent_platform
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --upgrade pip==26.1.2 \
&& python -m pip install .
EXPOSE 8001
HEALTHCHECK --interval=15s --timeout=5s --retries=10 CMD \
python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=3)"
# Access to the Docker API is the gateway's purpose. It is never shared with
# Open WebUI, the model runtime, or user workspace containers.
CMD ["uvicorn", "agent_platform.gateway.app:app", "--host", "0.0.0.0", "--port", "8001"]
+22
View File
@@ -0,0 +1,22 @@
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY pyproject.toml README.md ./
COPY agent_platform ./agent_platform
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --upgrade pip==26.1.2 \
&& python -m pip install .
RUN useradd --create-home --uid 10001 agent
WORKDIR /srv
USER 10001:10001
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=5s --retries=10 CMD \
python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"
CMD ["uvicorn", "agent_platform.runtime.app:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"]
+70
View File
@@ -0,0 +1,70 @@
# Open WebUI minimal backend dependencies for the K1412 Chat/Work surface.
# Optional RAG, media, browser automation, cloud storage, and vector providers
# are intentionally excluded.
fastapi==0.140.0
uvicorn[standard]==0.51.0
pydantic==2.13.4
python-multipart==0.0.31
itsdangerous==2.2.0
typer==0.21.0
ldap3==2.9.1
validators==0.35.0
python-socketio==5.16.2
cryptography==48.0.1
bcrypt==5.0.0
argon2-cffi==25.1.0
PyJWT[crypto]==2.13.0
authlib==1.6.12
requests==2.34.2
aiohttp==3.14.1
async-timeout==5.0.1
aiocache==0.12.3
aiofiles==25.1.0
starlette-compress==1.7.0
Brotli==1.2.0
brotlicffi==1.2.0.1
httpx[socks,http2,zstd,cli,brotli]==0.28.1
starsessions[redis]==2.2.1
python-mimeparse==2.0.0
sqlalchemy[asyncio]==2.0.51
aiosqlite==0.21.0
psycopg[binary]==3.2.9
alembic==1.18.4
peewee==3.19.0
peewee-migrate==1.14.3
pycrdt==0.12.47
redis==7.4.0
APScheduler==3.11.2
RestrictedPython==8.1
pytz==2026.1.post1
loguru==0.7.3
asgiref==3.11.1
python-dateutil==2.9.0.post0
tiktoken==0.12.0
mcp==1.28.1
openai==2.29.0
anthropic==0.87.0
huggingface-hub==1.16.1
langchain==1.3.9
langchain-community==0.4.1
langchain-classic==1.0.7
langchain-text-splitters==1.1.2
langsmith==0.8.18
fake-useragent==2.2.0
black==26.3.1
pydub==0.25.1
chardet==5.2.0
beautifulsoup4==4.14.3
nltk==3.10.0
pypdf==6.14.2
pymdown-extensions==11.0.0
pillow==12.3.0
ddgs==9.11.3
fpdf2==2.8.7
+76
View File
@@ -0,0 +1,76 @@
FROM node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3 AS web-build
ARG OPEN_WEBUI_REF=v0.9.6
ARG OPEN_WEBUI_COMMIT=1a97751e376e00a1897bc3679215ae1c7bd8fd42
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
RUN git clone --depth 1 --branch "${OPEN_WEBUI_REF}" https://github.com/open-webui/open-webui.git . \
&& test "$(git rev-parse HEAD)" = "${OPEN_WEBUI_COMMIT}"
COPY docker/web/ModelSelector.svelte src/lib/components/chat/ModelSelector.svelte
COPY docker/web/openwebui-no-rag.patch /tmp/openwebui-no-rag.patch
COPY docker/web/openwebui-local-storage.patch /tmp/openwebui-local-storage.patch
COPY docker/web/openwebui-lazy-azure.patch /tmp/openwebui-lazy-azure.patch
COPY docker/web/openwebui-product-ui.patch /tmp/openwebui-product-ui.patch
RUN git apply /tmp/openwebui-no-rag.patch \
&& git apply /tmp/openwebui-local-storage.patch \
&& git apply /tmp/openwebui-lazy-azure.patch \
&& git apply /tmp/openwebui-product-ui.patch
RUN npm ci --no-audit --no-fund \
&& npx vite build
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS backend-deps
RUN apt-get update \
&& apt-get upgrade --yes --no-install-recommends \
&& apt-get install --yes --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY docker/web-requirements.txt /tmp/web-requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --upgrade pip==26.1.2 \
&& python -m pip download --only-binary=:all: --dest /wheels pip==26.1.2 \
&& python -m pip wheel --wheel-dir /wheels --requirement /tmp/web-requirements.txt
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b
ENV PYTHONUNBUFFERED=1 \
ENV=prod \
PORT=8080 \
USE_OLLAMA_DOCKER=false \
USE_CUDA_DOCKER=false \
USE_SLIM_DOCKER=true \
SCARF_NO_ANALYTICS=true \
DO_NOT_TRACK=true \
ANONYMIZED_TELEMETRY=false \
VECTOR_DB=disabled \
DOCKER=true
RUN apt-get update \
&& apt-get upgrade --yes --no-install-recommends \
&& apt-get install --yes --no-install-recommends bash curl tini \
&& rm -rf /var/lib/apt/lists/*
COPY --from=backend-deps /wheels /wheels
COPY docker/web-requirements.txt /tmp/web-requirements.txt
RUN python -m pip install --no-cache-dir --no-index --find-links=/wheels --upgrade pip==26.1.2 \
&& python -m pip install --no-cache-dir --no-index --find-links=/wheels --requirement /tmp/web-requirements.txt \
&& rm -rf /wheels /tmp/web-requirements.txt
WORKDIR /app/backend
COPY --from=web-build /src/backend /app/backend
COPY --from=web-build /src/build /app/build
COPY --from=web-build /src/CHANGELOG.md /app/CHANGELOG.md
COPY --from=web-build /src/LICENSE /app/OPEN_WEBUI_LICENSE
COPY THIRD_PARTY_NOTICES.md /app/THIRD_PARTY_NOTICES.md
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=5s --retries=20 CMD \
curl --silent --fail http://127.0.0.1:8080/health
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["bash", "start.sh"]
+94
View File
@@ -0,0 +1,94 @@
<script lang="ts">
import { models } from '$lib/stores';
export let selectedModels = [''];
export let disabled = false;
export let showSetDefault = true;
const strengths = [
{ key: 'light', label: '轻度' },
{ key: 'medium', label: '中' },
{ key: 'high', label: '高' }
];
const allowed = new Set([
'chat-light',
'chat-medium',
'chat-high',
'work-light',
'work-medium',
'work-high'
]);
$: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'chat-medium';
$: mode = selected.startsWith('work-') ? 'work' : 'chat';
$: strength = selected.split('-')[1] ?? 'medium';
$: availableIds = new Set($models.map((model) => model.id));
$: if ($models.length > 0 && !availableIds.has(selectedModels?.[0])) {
const fallback = availableIds.has('chat-medium')
? 'chat-medium'
: [...allowed].find((id) => availableIds.has(id));
if (fallback) selectedModels = [fallback];
}
const chooseMode = (nextMode: 'chat' | 'work') => {
if (disabled || (mode === 'work' && nextMode === 'chat')) return;
const next = `${nextMode}-${strength}`;
if (availableIds.has(next)) selectedModels = [next];
};
const chooseStrength = (nextStrength: string) => {
if (disabled) return;
const next = `${mode}-${nextStrength}`;
if (availableIds.has(next)) selectedModels = [next];
};
</script>
<div class="flex max-w-fit items-center gap-2 select-none">
<div
class="flex items-center rounded-xl bg-gray-100/90 p-1 text-sm dark:bg-gray-800/90"
aria-label="Agent mode"
>
<button
type="button"
class="rounded-lg px-3 py-1.5 font-medium transition {mode === 'chat'
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
: 'text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => chooseMode('chat')}
disabled={disabled || mode === 'work'}
title={mode === 'work' ? '此对话已升级为 Work,不能返回 Chat' : '快速对话'}
>
Chat
</button>
<button
type="button"
class="rounded-lg px-3 py-1.5 font-medium transition {mode === 'work'
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
: 'text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => chooseMode('work')}
disabled={disabled}
title="长链路、多步骤工作"
>
Work
</button>
</div>
<div
class="flex items-center rounded-xl border border-gray-200/80 p-1 text-xs dark:border-gray-700/80"
aria-label="Inference strength"
>
{#each strengths as item}
<button
type="button"
class="rounded-lg px-2.5 py-1.5 transition {strength === item.key
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
on:click={() => chooseStrength(item.key)}
disabled={disabled}
>
{item.label}
</button>
{/each}
</div>
</div>
+21
View File
@@ -0,0 +1,21 @@
diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py
index 9c1325e..a19f128 100644
--- a/backend/open_webui/routers/openai.py
+++ b/backend/open_webui/routers/openai.py
@@ -10,7 +10,6 @@ from urllib.parse import quote, urlparse
import aiohttp
from aiocache import cached
-from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import (
FileResponse,
@@ -218,6 +217,8 @@ def get_microsoft_entra_id_access_token():
Get Microsoft Entra ID access token using DefaultAzureCredential for Azure OpenAI.
Returns the token string or None if authentication fails.
"""
+ from azure.identity import DefaultAzureCredential, get_bearer_token_provider
+
try:
token_provider = get_bearer_token_provider(
DefaultAzureCredential(), 'https://cognitiveservices.azure.com/.default'
+44
View File
@@ -0,0 +1,44 @@
diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py
index 261f010..048ea4b 100644
--- a/backend/open_webui/storage/provider.py
+++ b/backend/open_webui/storage/provider.py
@@ -6,14 +6,6 @@ import shutil
from abc import ABC, abstractmethod
from typing import BinaryIO, Dict, Tuple
-import boto3
-from azure.core.exceptions import ResourceNotFoundError
-from azure.identity import DefaultAzureCredential
-from azure.storage.blob import BlobServiceClient
-from botocore.config import Config
-from botocore.exceptions import ClientError
-from google.cloud import storage
-from google.cloud.exceptions import GoogleCloudError, NotFound
from open_webui.config import (
AZURE_STORAGE_CONTAINER_NAME,
AZURE_STORAGE_ENDPOINT,
@@ -335,10 +327,24 @@ def get_storage_provider(storage_provider: str):
if storage_provider == 'local':
Storage = LocalStorageProvider()
elif storage_provider == 's3':
+ global boto3, Config, ClientError
+ import boto3
+ from botocore.config import Config
+ from botocore.exceptions import ClientError
+
Storage = S3StorageProvider()
elif storage_provider == 'gcs':
+ global storage, GoogleCloudError, NotFound
+ from google.cloud import storage
+ from google.cloud.exceptions import GoogleCloudError, NotFound
+
Storage = GCSStorageProvider()
elif storage_provider == 'azure':
+ global ResourceNotFoundError, DefaultAzureCredential, BlobServiceClient
+ from azure.core.exceptions import ResourceNotFoundError
+ from azure.identity import DefaultAzureCredential
+ from azure.storage.blob import BlobServiceClient
+
Storage = AzureStorageProvider()
else:
raise RuntimeError(f'Unsupported storage provider: {storage_provider}')
+71
View File
@@ -0,0 +1,71 @@
diff --git a/backend/open_webui/retrieval/vector/dbs/disabled.py b/backend/open_webui/retrieval/vector/dbs/disabled.py
new file mode 100644
index 0000000..7e1ab23
--- /dev/null
+++ b/backend/open_webui/retrieval/vector/dbs/disabled.py
@@ -0,0 +1,50 @@
+from typing import Dict, List, Optional, Union
+
+from open_webui.retrieval.vector.main import (
+ GetResult,
+ SearchResult,
+ VectorDBBase,
+ VectorItem,
+)
+
+
+class DisabledVectorClient(VectorDBBase):
+ """No-op vector backend used when all RAG surfaces are disabled."""
+
+ def has_collection(self, collection_name: str) -> bool:
+ return False
+
+ def delete_collection(self, collection_name: str) -> None:
+ return None
+
+ def insert(self, collection_name: str, items: List[VectorItem]) -> None:
+ raise RuntimeError("Vector storage is disabled")
+
+ def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
+ raise RuntimeError("Vector storage is disabled")
+
+ def search(
+ self,
+ collection_name: str,
+ vectors: List[List[Union[float, int]]],
+ filter: Optional[Dict] = None,
+ limit: int = 10,
+ ) -> Optional[SearchResult]:
+ return None
+
+ def query(
+ self,
+ collection_name: str,
+ filter: Dict,
+ limit: Optional[int] = None,
+ ) -> Optional[GetResult]:
+ return None
+
+ def get(self, collection_name: str) -> Optional[GetResult]:
+ return None
+
+ def delete(self, collection_name: str, ids=None, filter=None) -> None:
+ return None
+
+ def reset(self) -> None:
+ return None
diff --git a/backend/open_webui/retrieval/vector/factory.py b/backend/open_webui/retrieval/vector/factory.py
index af10c5d..35c3266 100644
--- a/backend/open_webui/retrieval/vector/factory.py
+++ b/backend/open_webui/retrieval/vector/factory.py
@@ -74,6 +74,10 @@ class Vector:
case VectorType.CHROMA:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
return ChromaClient()
+ case 'disabled':
+ from open_webui.retrieval.vector.dbs.disabled import DisabledVectorClient
+
+ return DisabledVectorClient()
case VectorType.ORACLE23AI:
from open_webui.retrieval.vector.dbs.oracle23ai import Oracle23aiClient
+15
View File
@@ -0,0 +1,15 @@
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index 95bf320e..461618ed 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -316,9 +316,7 @@
};
setupKeyboardShortcuts();
- if ($user?.role === 'admin' && ($settings?.showChangelog ?? true)) {
- showChangelog.set($settings?.version !== $config.version);
- }
+ showChangelog.set(false);
if ($user?.role === 'admin' || ($user?.permissions?.chat?.temporary ?? true)) {
if ($page.url.searchParams.get('temporary-chat') === 'true') {
+51
View File
@@ -0,0 +1,51 @@
FROM node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3 AS node-runtime
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
build-essential \
ca-certificates \
curl \
git \
jq \
less \
libatomic1 \
openssh-client \
patch \
ripgrep \
tini \
&& rm -rf /var/lib/apt/lists/*
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
COPY --from=node-runtime /usr/local/include/node /usr/local/include/node
COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
&& ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack \
&& npm install --global npm@12.0.1 \
&& npm pack --silent brace-expansion@5.0.8 --pack-destination /tmp \
&& rm -rf /usr/local/lib/node_modules/npm/node_modules/brace-expansion \
&& mkdir -p /usr/local/lib/node_modules/npm/node_modules/brace-expansion \
&& tar -xzf /tmp/brace-expansion-5.0.8.tgz \
-C /usr/local/lib/node_modules/npm/node_modules/brace-expansion \
--strip-components=1 \
&& rm /tmp/brace-expansion-5.0.8.tgz \
&& npm cache clean --force \
&& node --version \
&& npm --version
RUN python -m pip install --no-cache-dir --upgrade pip==26.1.2
RUN groupadd --gid 1000 agent \
&& useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash agent \
&& mkdir -p /workspace \
&& chown 1000:1000 /workspace
WORKDIR /workspace
USER 1000:1000
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["sleep", "infinity"]