This commit is contained in:
Timothy Jaeryang Baek 2026-10-10 18:51:24 +04:00
parent 371ed44b2c
commit a3cd6158bb
4 changed files with 93 additions and 5 deletions

View file

@ -237,7 +237,7 @@ def parse_import(
'name': str(name),
'description': package.get('description', fm.get('description', '')),
'meta': package.get('meta') or {},
'is_active': package.get('is_active', True),
**({'is_active': package['is_active']} if 'is_active' in package else {}),
'files': files,
}
)

View file

@ -43,6 +43,40 @@
import Badge from '$lib/components/common/Badge.svelte';
import Switch from '../common/Switch.svelte';
import SkillMenu from './Skills/SkillMenu.svelte';
import CommunityDiscover from './common/CommunityDiscover.svelte';
import { config } from '$lib/stores';
let stopSkillShare = () => {};
const shareHandler = (skill: { id: string }) => {
stopSkillShare();
const tab = window.open('https://openwebui.com/post?type=skill', '_blank');
if (!tab) {
toast.error($i18n.t('Please allow popups to share your skill.'));
return;
}
const receiveLoaded = async (event: MessageEvent) => {
if (
event.origin !== 'https://openwebui.com' ||
event.source !== tab ||
event.data !== 'loaded'
)
return;
stopSkillShare();
try {
const exported = JSON.parse(
await (await exportSkillBundle(localStorage.token, 'json', [skill.id])).text()
);
const { id, name, description, files } = Array.isArray(exported) ? exported[0] : exported;
const data = JSON.stringify({ id, name, description, files });
if (new Blob([data]).size > 15 * 1024 * 1024)
throw new Error('Community skill JSON exceeds 15 MiB');
tab.postMessage(data, 'https://openwebui.com');
} catch (error) {
toast.error(skillError(error));
}
};
window.addEventListener('message', receiveLoaded);
stopSkillShare = () => window.removeEventListener('message', receiveLoaded);
};
import SkillImport from './Skills/SkillImport.svelte';
import ImportModal from '$lib/components/ImportModal.svelte';
import { cloneSkill, exportSkillBundle, loadSkillByUrl, skillError } from '$lib/apis/skills';
@ -289,6 +323,7 @@
});
onDestroy(() => {
stopSkillShare();
searchController?.abort();
clearTimeout(searchDebounceTimer);
});
@ -564,6 +599,7 @@
{:else}
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
<SkillMenu
shareHandler={() => shareHandler(skill)}
accessHandler={() => accessModal.open(skill.id)}
show={openSkillMenuId === skill.id}
editHandler={() => {
@ -655,6 +691,13 @@
<span class=" font-normal">{resolveLocalizedResource(selectedSkill, $i18n.language)}</span>.
</div>
</DeleteConfirmDialog>
{#if $config?.features?.enable_community_sharing}
<CommunityDiscover
href="https://openwebui.com/search?type=skill"
title={$i18n.t('Discover a skill')}
description={$i18n.t('Discover, download, and explore community skills')}
/>
{/if}
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />

View file

@ -8,13 +8,14 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import Download from '$lib/components/icons/Download.svelte';
import { user } from '$lib/stores';
import { user, config } from '$lib/stores';
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let editHandler: Function;
export let cloneHandler: Function;
export let exportHandler: Function;
export let shareHandler: (() => void) | null = null;
export let deleteHandler: Function;
export let accessHandler: (() => void) | null = null;
export let onClose: Function;
@ -90,6 +91,16 @@
<div class="flex items-center">{$i18n.t('Clone')}</div>
</button>
{#if shareHandler && $config?.features?.enable_community_sharing && ($user?.role === 'admin' || $user?.permissions?.workspace?.skills_export)}
<button
type="button"
class="flex h-7 w-full items-center gap-2 px-2 text-[0.8125rem]"
on:click={() => {
shareHandler?.();
closeMenu();
}}>{$i18n.t('Share to Community')}</button
>
{/if}
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.skills_export}
<button
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { skills } from '$lib/stores';
import { skills, user, config } from '$lib/stores';
import { COMMUNITY_ORIGINS } from '$lib/constants';
import { onMount, getContext } from 'svelte';
const i18n = getContext<any>('i18n');
@ -32,7 +32,40 @@
}
};
onMount(async () => {
onMount(() => {
const receiveSkill = (event: MessageEvent) => {
if (!COMMUNITY_ORIGINS.includes(event.origin) || event.source !== window.opener) return;
if (
!$config?.features?.enable_community_sharing ||
($user?.role !== 'admin' && !$user?.permissions?.workspace?.skills_import)
)
return;
try {
const data = JSON.parse(event.data);
if (
!Array.isArray(data.files) ||
typeof data.id !== 'string' ||
typeof data.name !== 'string'
)
throw new Error('Invalid skill JSON');
skill = {
id: data.id,
name: data.name,
description: data.description ?? '',
files: data.files,
content:
data.files.find((file: { path: string }) => file.path === 'SKILL.md')?.content ?? '',
is_active: true,
access_grants: []
};
window.removeEventListener('message', receiveSkill);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Invalid skill JSON');
}
};
window.addEventListener('message', receiveSkill);
if (window.opener) window.opener.postMessage('loaded', '*');
if (sessionStorage.skill) {
const _skill = JSON.parse(sessionStorage.skill);
sessionStorage.removeItem('skill');
@ -49,6 +82,7 @@
access_grants: _skill.access_grants !== undefined ? _skill.access_grants : []
};
}
return () => window.removeEventListener('message', receiveSkill);
});
</script>