feat:精简
Some checks failed
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Python CI / Format Backend (3.11.x) (push) Has been cancelled
Python CI / Format Backend (3.12.x) (push) Has been cancelled
Frontend Build / Format & Build Frontend (push) Has been cancelled
Frontend Build / Frontend Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda126-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-ollama-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-slim-images (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled

This commit is contained in:
2026-01-16 18:34:38 +08:00
parent 16263710d9
commit 11fcec9387
137 changed files with 68993 additions and 6435 deletions

View File

@@ -22,7 +22,7 @@ from typing import Optional, Union, List, Dict
from opentelemetry import trace
from open_webui.config import WEBUI_URL
from open_webui.config import WEBUI_URL, EMAIL_VERIFY_TEMPLATE, EMAIL_CODE_TEMPLATE
from open_webui.utils.smtp import send_email
from open_webui.utils.access_control import has_permission
@@ -47,6 +47,7 @@ from open_webui.env import (
REDIS_SENTINEL_PORT,
WEBUI_NAME,
REDIS_CLUSTER,
BASE_DIR,
)
from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
@@ -81,76 +82,176 @@ def verify_signature(payload: str, signature: str) -> bool:
return False
def override_static(path: str, content: str):
def override_static(path: str, content: str) -> bool:
"""Download content from URL and save to path.
Returns True if successful, False otherwise.
Does not raise exceptions to prevent app startup failure.
"""
os.makedirs(os.path.dirname(path), exist_ok=True)
r = requests.get(content, stream=True)
with open(path, "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
try:
# try with SSL verification first, then without if it fails
try:
r = requests.get(content, stream=True, timeout=30)
except requests.exceptions.SSLError:
log.warning(f"SSL error downloading {content}, retrying without verification")
r = requests.get(content, stream=True, timeout=30, verify=False)
r.raise_for_status()
with open(path, "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
log.info(f"Successfully downloaded {content} to {path}")
return True
except Exception as e:
log.error(f"Failed to download {content}: {e}")
return False
def apply_branding(app):
"""Apply branding settings from database config or environment variables."""
custom_png = ""
custom_svg = ""
custom_ico = ""
custom_dark_png = ""
organization_name = "OpenWebui"
custom_name = ""
if hasattr(app, "state") and hasattr(app.state, "config"):
# Get config values - must access _state directly to get fresh PersistentConfig values
def get_config_value(attr_name, default=""):
# Access _state directly to get the PersistentConfig object
config_obj = app.state.config._state.get(attr_name)
if config_obj is None:
return default
# If it's a PersistentConfig object, get its value
if hasattr(config_obj, "value"):
result = config_obj.value or default
log.info(f"Branding: {attr_name}.value = {result}")
return result
return config_obj or default
custom_png = get_config_value("BRANDING_FAVICON_PNG") or os.getenv("CUSTOM_PNG", "")
custom_svg = get_config_value("BRANDING_FAVICON_SVG") or os.getenv("CUSTOM_SVG", "")
custom_ico = get_config_value("BRANDING_FAVICON_ICO") or os.getenv("CUSTOM_ICO", "")
custom_dark_png = get_config_value("BRANDING_FAVICON_DARK_PNG") or os.getenv("CUSTOM_DARK_PNG", "")
organization_name = get_config_value("BRANDING_ORGANIZATION_NAME") or os.getenv("ORGANIZATION_NAME", "OpenWebui")
custom_name = get_config_value("BRANDING_CUSTOM_NAME") or os.getenv("CUSTOM_NAME", "")
else:
custom_png = os.getenv("CUSTOM_PNG", "")
custom_svg = os.getenv("CUSTOM_SVG", "")
custom_ico = os.getenv("CUSTOM_ICO", "")
custom_dark_png = os.getenv("CUSTOM_DARK_PNG", "")
organization_name = os.getenv("ORGANIZATION_NAME", "OpenWebui")
custom_name = os.getenv("CUSTOM_NAME", "")
log.info(f"Branding: custom_name={custom_name}, custom_png={custom_png}")
# Frontend static/static directory (for dev mode)
# SvelteKit maps static/ folder to root, so /static/favicon.png = static/static/favicon.png
frontend_static_dir = BASE_DIR / "static" / "static"
# Build resources mapping for both backend and frontend static dirs
resources = []
# files to override
branding_files = [
("logo.png", custom_png),
("favicon.png", custom_png),
("favicon.svg", custom_svg),
("favicon-96x96.png", custom_png),
("apple-touch-icon.png", custom_png),
("web-app-manifest-192x192.png", custom_png),
("web-app-manifest-512x512.png", custom_png),
("splash.png", custom_png),
("favicon.ico", custom_ico),
("favicon-dark.png", custom_dark_png),
("splash-dark.png", custom_dark_png),
]
for filename, url in branding_files:
if url:
# Add backend static dir path
resources.append((os.path.join(STATIC_DIR, filename), url))
# Add frontend static/static dir path (for dev mode)
resources.append((os.path.join(frontend_static_dir, filename), url))
try:
for path, url in resources:
if url:
log.info(f"Branding: Downloading {url} to {path}")
override_static(path, url)
# set metadata
setattr(app.state, "LICENSE_METADATA", {
"type": "enterprise",
"organization_name": organization_name,
})
log.info(f"Branding: Set LICENSE_METADATA organization_name={organization_name}")
# set custom name if provided
if custom_name:
setattr(app.state, "WEBUI_NAME", custom_name)
log.info(f"Branding: Set WEBUI_NAME={custom_name}")
else:
log.info(f"Branding: custom_name is empty, WEBUI_NAME not changed")
return True
except Exception as ex:
log.exception(f"Branding: Uncaught Exception: {ex}")
return False
def get_license_data(app, key):
# get branding config from app.state.config if available, fallback to env vars
custom_png = ""
custom_svg = ""
custom_ico = ""
custom_dark_png = ""
organization_name = "OpenWebui"
if hasattr(app, "state") and hasattr(app.state, "config"):
custom_png = getattr(app.state.config, "BRANDING_FAVICON_PNG", "") or os.getenv("CUSTOM_PNG", "")
custom_svg = getattr(app.state.config, "BRANDING_FAVICON_SVG", "") or os.getenv("CUSTOM_SVG", "")
custom_ico = getattr(app.state.config, "BRANDING_FAVICON_ICO", "") or os.getenv("CUSTOM_ICO", "")
custom_dark_png = getattr(app.state.config, "BRANDING_FAVICON_DARK_PNG", "") or os.getenv("CUSTOM_DARK_PNG", "")
organization_name = getattr(app.state.config, "BRANDING_ORGANIZATION_NAME", "") or os.getenv("ORGANIZATION_NAME", "OpenWebui")
else:
custom_png = os.getenv("CUSTOM_PNG", "")
custom_svg = os.getenv("CUSTOM_SVG", "")
custom_ico = os.getenv("CUSTOM_ICO", "")
custom_dark_png = os.getenv("CUSTOM_DARK_PNG", "")
organization_name = os.getenv("ORGANIZATION_NAME", "OpenWebui")
payload = {
"resources": {
os.path.join(STATIC_DIR, "logo.png"): os.getenv("CUSTOM_PNG", ""),
os.path.join(STATIC_DIR, "favicon.png"): os.getenv("CUSTOM_PNG", ""),
os.path.join(STATIC_DIR, "favicon.svg"): os.getenv("CUSTOM_SVG", ""),
os.path.join(STATIC_DIR, "favicon-96x96.png"): os.getenv("CUSTOM_PNG", ""),
os.path.join(STATIC_DIR, "apple-touch-icon.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(STATIC_DIR, "web-app-manifest-192x192.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(STATIC_DIR, "web-app-manifest-512x512.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(STATIC_DIR, "splash.png"): os.getenv("CUSTOM_PNG", ""),
os.path.join(STATIC_DIR, "favicon.ico"): os.getenv("CUSTOM_ICO", ""),
os.path.join(STATIC_DIR, "favicon-dark.png"): os.getenv(
"CUSTOM_DARK_PNG", ""
),
os.path.join(STATIC_DIR, "splash-dark.png"): os.getenv(
"CUSTOM_DARK_PNG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "favicon.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/favicon.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/favicon.svg"): os.getenv(
"CUSTOM_SVG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/favicon-96x96.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/apple-touch-icon.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(
FRONTEND_BUILD_DIR, "static/web-app-manifest-192x192.png"
): os.getenv("CUSTOM_PNG", ""),
os.path.join(
FRONTEND_BUILD_DIR, "static/web-app-manifest-512x512.png"
): os.getenv("CUSTOM_PNG", ""),
os.path.join(FRONTEND_BUILD_DIR, "static/splash.png"): os.getenv(
"CUSTOM_PNG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/favicon.ico"): os.getenv(
"CUSTOM_ICO", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/favicon-dark.png"): os.getenv(
"CUSTOM_DARK_PNG", ""
),
os.path.join(FRONTEND_BUILD_DIR, "static/splash-dark.png"): os.getenv(
"CUSTOM_DARK_PNG", ""
),
os.path.join(STATIC_DIR, "logo.png"): custom_png,
os.path.join(STATIC_DIR, "favicon.png"): custom_png,
os.path.join(STATIC_DIR, "favicon.svg"): custom_svg,
os.path.join(STATIC_DIR, "favicon-96x96.png"): custom_png,
os.path.join(STATIC_DIR, "apple-touch-icon.png"): custom_png,
os.path.join(STATIC_DIR, "web-app-manifest-192x192.png"): custom_png,
os.path.join(STATIC_DIR, "web-app-manifest-512x512.png"): custom_png,
os.path.join(STATIC_DIR, "splash.png"): custom_png,
os.path.join(STATIC_DIR, "favicon.ico"): custom_ico,
os.path.join(STATIC_DIR, "favicon-dark.png"): custom_dark_png,
os.path.join(STATIC_DIR, "splash-dark.png"): custom_dark_png,
os.path.join(FRONTEND_BUILD_DIR, "favicon.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/favicon.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/favicon.svg"): custom_svg,
os.path.join(FRONTEND_BUILD_DIR, "static/favicon-96x96.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/apple-touch-icon.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/web-app-manifest-192x192.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/web-app-manifest-512x512.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/splash.png"): custom_png,
os.path.join(FRONTEND_BUILD_DIR, "static/favicon.ico"): custom_ico,
os.path.join(FRONTEND_BUILD_DIR, "static/favicon-dark.png"): custom_dark_png,
os.path.join(FRONTEND_BUILD_DIR, "static/splash-dark.png"): custom_dark_png,
},
"metadata": {
"type": "enterprise",
"organization_name": os.getenv("ORGANIZATION_NAME", "OpenWebui"),
"organization_name": organization_name,
},
}
try:
@@ -611,11 +712,17 @@ def send_verify_email(email: str):
code = f"{uuid.uuid4().hex}{uuid.uuid1().hex}"
redis.set(name=get_email_code_key(code=code), value=email, ex=timedelta(days=1))
link = f"{WEBUI_URL.value.rstrip('/')}/api/v1/auths/signup_verify/{code}"
# use template from config
template = EMAIL_VERIFY_TEMPLATE.value or verify_email_template
# use replace instead of % formatting to avoid issues with % in CSS
body = template.replace("%(title)s", f"{WEBUI_NAME} Email Verify").replace("%(link)s", link)
send_email(
receiver=email,
subject=f"{WEBUI_NAME} Email Verify",
body=verify_email_template
% {"title": f"{WEBUI_NAME} Email Verify", "link": link},
body=body,
)
@@ -628,3 +735,78 @@ def verify_email_by_code(code: str) -> str:
redis_cluster=REDIS_CLUSTER,
)
return redis.get(name=get_email_code_key(code=code))
# email verification code template
email_code_template = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }
.container { max-width: 500px; margin: 0 auto; background: white; border-radius: 10px; padding: 30px; }
.code { font-size: 32px; font-weight: bold; color: #3b82f6; letter-spacing: 8px; text-align: center; padding: 20px; background: #f0f9ff; border-radius: 8px; margin: 20px 0; }
.note { color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h2>%(title)s</h2>
<p>您的验证码是:</p>
<div class="code">%(code)s</div>
<p class="note">验证码有效期为10分钟请勿泄露给他人。</p>
</div>
</body>
</html>"""
def get_signup_code_key(email: str) -> str:
return f"signup_code:{email}"
def send_signup_email_code(email: str) -> str:
"""Send a 6-digit verification code to email for signup."""
import random
redis = get_redis_connection(
redis_url=REDIS_URL,
redis_sentinels=get_sentinels_from_env(
REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT
),
redis_cluster=REDIS_CLUSTER,
)
# generate 6-digit code
code = str(random.randint(100000, 999999))
# store in redis with 10 min expiration
redis.set(name=get_signup_code_key(email.lower()), value=code, ex=timedelta(minutes=10))
# use template from config
template = EMAIL_CODE_TEMPLATE.value or email_code_template
send_email(
receiver=email,
subject=f"{WEBUI_NAME} 注册验证码",
body=template % {"title": f"{WEBUI_NAME} 注册验证码", "code": code},
)
return code
def verify_signup_email_code(email: str, code: str) -> bool:
"""Verify the signup email code."""
redis = get_redis_connection(
redis_url=REDIS_URL,
redis_sentinels=get_sentinels_from_env(
REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT
),
redis_cluster=REDIS_CLUSTER,
)
stored_code = redis.get(name=get_signup_code_key(email.lower()))
if stored_code and stored_code == code:
# delete code after successful verification
redis.delete(get_signup_code_key(email.lower()))
return True
return False