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
+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') {