mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-14 04:08:46 +00:00
refac
This commit is contained in:
parent
0b1e67c38e
commit
d823ebd96d
9 changed files with 78 additions and 10 deletions
|
|
@ -1,12 +1,14 @@
|
|||
"""External apps. Credentials never leave the internal model."""
|
||||
|
||||
import base64
|
||||
import re
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
from open_webui.internal.db import Base, get_async_db_context
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.channels import ChannelMember
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, SecretStr, field_validator
|
||||
from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
|
@ -54,9 +56,26 @@ class Manifest(StrictModel):
|
|||
id: str = Field(min_length=1, max_length=100)
|
||||
name: str = Field(min_length=1, max_length=100)
|
||||
description: str = Field(default='', max_length=2000)
|
||||
profile_image_url: str | None = Field(default=None, max_length=196608)
|
||||
public_key: str = Field(max_length=2000)
|
||||
requested_scopes: list[Scope]
|
||||
|
||||
@field_validator('profile_image_url')
|
||||
@classmethod
|
||||
def validate_profile_image_url(cls, value):
|
||||
if value is not None:
|
||||
if value.startswith('data:'):
|
||||
header, encoded = value.split(',', 1)
|
||||
if not re.fullmatch(r'data:image/(png|jpeg|gif|webp);base64', header):
|
||||
raise ValueError('Expected a base64 PNG, JPEG, GIF or WebP data URL')
|
||||
if not base64.b64decode(encoded, validate=True):
|
||||
raise ValueError('Profile image data must not be empty')
|
||||
else:
|
||||
url = HttpUrl(value)
|
||||
if len(value) > 2048 or url.username is not None or url.password is not None:
|
||||
raise ValueError('Profile image URL must not contain credentials or exceed 2048 characters')
|
||||
return value
|
||||
|
||||
@field_validator('public_key')
|
||||
@classmethod
|
||||
def validate_public_key(cls, value):
|
||||
|
|
|
|||
|
|
@ -295,7 +295,12 @@ class MessageTable:
|
|||
if message.app_id:
|
||||
from open_webui.models.apps import App
|
||||
app = await db.get(App, message.app_id)
|
||||
return {'id': message.app_id, 'name': app.name if app else 'Removed App', 'role': 'app'}
|
||||
return {
|
||||
'id': message.app_id,
|
||||
'name': app.name if app else 'Removed App',
|
||||
'role': 'app',
|
||||
'profile_image_url': app.data.get('manifest', {}).get('profile_image_url') if app else None,
|
||||
}
|
||||
webhook_info = message.meta.get('webhook') if message.meta else None
|
||||
if webhook_info and webhook_info.get('id'):
|
||||
webhook = await Channels.get_webhook_by_id(webhook_info.get('id'), db=db)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export type App = {
|
|||
url: string;
|
||||
manifest: {
|
||||
description: string;
|
||||
profile_image_url?: string | null;
|
||||
};
|
||||
granted_scopes: string[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getApps, updateAppById, type App } from '$lib/apis/apps';
|
||||
|
|
@ -73,7 +74,13 @@
|
|||
? ''
|
||||
: 'opacity-50'}"
|
||||
>
|
||||
<Grid className="size-4 shrink-0" strokeWidth="1.5" />
|
||||
{#if app.data.manifest.profile_image_url}
|
||||
<ProfileImage
|
||||
src={app.data.manifest.profile_image_url}
|
||||
allowExternal
|
||||
className="size-4 shrink-0 rounded!"
|
||||
/>
|
||||
{:else}<Grid className="size-4 shrink-0" strokeWidth="1.5" />{/if}
|
||||
<span class="truncate"
|
||||
>{app.name}
|
||||
<span class="text-gray-400 dark:text-gray-600">{app.slug}</span></span
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user as _user, showSidebar } from '$lib/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -229,7 +230,13 @@
|
|||
<div
|
||||
class="size-6 rounded-lg bg-gray-100 dark:bg-gray-850 flex items-center justify-center shrink-0"
|
||||
>
|
||||
<Grid className="size-3.5" />
|
||||
{#if app.data.manifest.profile_image_url}
|
||||
<ProfileImage
|
||||
src={app.data.manifest.profile_image_url}
|
||||
allowExternal
|
||||
className="size-6 rounded-lg!"
|
||||
/>
|
||||
{:else}<Grid className="size-3.5" />{/if}
|
||||
</div>
|
||||
<span class="truncate">{app.name}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
||||
import { getAbortSignal, getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
let _models = [];
|
||||
let _users = [];
|
||||
let _channels = [];
|
||||
let _apps: { type: string; id: string; label: string }[] = [];
|
||||
let _apps: { type: string; id: string; label: string; profile_image_url?: string | null }[] = [];
|
||||
|
||||
type UserSuggestion = { id: string; name: string };
|
||||
|
||||
|
|
@ -141,7 +142,12 @@
|
|||
.then(([apps, joined]) => {
|
||||
_apps = apps
|
||||
.filter((a) => a.is_active && joined.some((member) => member.id === a.id))
|
||||
.map((a) => ({ type: 'app', id: a.id, label: a.name }));
|
||||
.map((a) => ({
|
||||
type: 'app',
|
||||
id: a.id,
|
||||
label: a.name,
|
||||
profile_image_url: a.data.manifest.profile_image_url
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
|
@ -240,7 +246,13 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else if item.type === 'app'}
|
||||
<Grid className="size-4 mr-2" />
|
||||
{#if item.profile_image_url}
|
||||
<ProfileImage
|
||||
src={item.profile_image_url}
|
||||
allowExternal
|
||||
className="size-5 mr-2 rounded-lg!"
|
||||
/>
|
||||
{:else}<Grid className="size-4 mr-2" />{/if}
|
||||
{:else if item.type === 'model'}
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${item.id}&lang=${$i18n.language}`}
|
||||
|
|
|
|||
|
|
@ -400,7 +400,16 @@
|
|||
e.currentTarget.src = '/favicon.png';
|
||||
}}
|
||||
/>
|
||||
{:else if message.app_id}<span class="flex size-8 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800 text-xs" aria-label="App">App</span>
|
||||
{:else if message.app_id}
|
||||
{#if message.user?.profile_image_url}
|
||||
<ProfileImage
|
||||
src={message.user.profile_image_url}
|
||||
allowExternal
|
||||
className="size-6 mt-0.5 rounded-lg!"
|
||||
/>
|
||||
{:else}
|
||||
<span class="flex size-6 mt-0.5 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-800 text-xs" aria-label="App">App</span>
|
||||
{/if}
|
||||
{:else if message.user?.role === 'webhook'}
|
||||
<ProfileImage
|
||||
src={`${WEBUI_API_BASE_URL}/channels/webhooks/${message.user?.id}/profile/image`}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
const FALLBACK_SRC = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
|
||||
export let src = FALLBACK_SRC;
|
||||
export let allowExternal = false;
|
||||
</script>
|
||||
|
||||
<img
|
||||
aria-hidden="true"
|
||||
src={safeImageUrl(src)}
|
||||
src={safeImageUrl(src, allowExternal)}
|
||||
class=" {className} object-cover rounded-2xl"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount, onDestroy } from 'svelte';
|
||||
|
||||
|
|
@ -357,7 +358,13 @@
|
|||
<div
|
||||
class="size-6 rounded-lg bg-gray-100 dark:bg-gray-850 flex items-center justify-center shrink-0"
|
||||
>
|
||||
<Grid className="size-3.5" />
|
||||
{#if app.data.manifest.profile_image_url}
|
||||
<ProfileImage
|
||||
src={app.data.manifest.profile_image_url}
|
||||
allowExternal
|
||||
className="size-6 rounded-lg!"
|
||||
/>
|
||||
{:else}<Grid className="size-3.5" />{/if}
|
||||
</div>
|
||||
<span class="truncate">{app.name}</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue