mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-20 20:52:23 -05:00
refac
This commit is contained in:
@@ -358,6 +358,24 @@ class TerminalServerPolicyForm(BaseModel):
|
||||
policy_data: dict
|
||||
|
||||
|
||||
class TerminalServerLifecycleForm(BaseModel):
|
||||
url: str
|
||||
key: str | None = ''
|
||||
auth_type: str | None = 'bearer'
|
||||
policy_id: str
|
||||
lifecycle_data: dict
|
||||
|
||||
|
||||
class TerminalServerRefreshForm(BaseModel):
|
||||
url: str
|
||||
key: str | None = ''
|
||||
auth_type: str | None = 'bearer'
|
||||
user_id: str | None = None
|
||||
policy_id: str | None = None
|
||||
only_idle: bool = True
|
||||
reset: bool = False
|
||||
|
||||
|
||||
@router.post('/terminal_servers/policy')
|
||||
async def put_terminal_server_policy(
|
||||
request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user)
|
||||
@@ -393,6 +411,91 @@ async def put_terminal_server_policy(
|
||||
raise HTTPException(status_code=400, detail='Failed to save policy to terminal server')
|
||||
|
||||
|
||||
@router.post('/terminal_servers/lifecycle')
|
||||
async def put_terminal_server_lifecycle(
|
||||
request: Request, form_data: TerminalServerLifecycleForm, user=Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
Proxy a policy lifecycle PUT to an orchestrator terminal server.
|
||||
"""
|
||||
base_url = (form_data.url or '').rstrip('/')
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=400, detail='Terminal server URL is required')
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if form_data.auth_type == 'bearer' and form_data.key:
|
||||
headers['Authorization'] = f'Bearer {form_data.key}'
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
|
||||
) as session:
|
||||
lifecycle_url = f'{base_url}/api/v1/policies/{form_data.policy_id}/lifecycle'
|
||||
async with session.put(
|
||||
lifecycle_url,
|
||||
headers=headers,
|
||||
json=form_data.lifecycle_data,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as resp:
|
||||
if resp.ok:
|
||||
return await resp.json()
|
||||
detail = await resp.text()
|
||||
raise HTTPException(status_code=resp.status, detail=detail)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.debug(f'Failed to save lifecycle to terminal server: {e}')
|
||||
raise HTTPException(status_code=400, detail='Failed to save lifecycle to terminal server')
|
||||
|
||||
|
||||
@router.post('/terminal_servers/refresh')
|
||||
async def refresh_terminal_server_terminals(
|
||||
request: Request, form_data: TerminalServerRefreshForm, user=Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
Proxy a terminal refresh request to an orchestrator terminal server.
|
||||
"""
|
||||
base_url = (form_data.url or '').rstrip('/')
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=400, detail='Terminal server URL is required')
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if form_data.auth_type == 'bearer' and form_data.key:
|
||||
headers['Authorization'] = f'Bearer {form_data.key}'
|
||||
|
||||
body = {
|
||||
'only_idle': form_data.only_idle,
|
||||
'reset': form_data.reset,
|
||||
}
|
||||
if form_data.user_id:
|
||||
body['user_id'] = form_data.user_id
|
||||
if form_data.policy_id:
|
||||
body['policy_id'] = form_data.policy_id
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
|
||||
) as session:
|
||||
refresh_url = f'{base_url}/api/v1/terminals/refresh'
|
||||
async with session.post(
|
||||
refresh_url,
|
||||
headers=headers,
|
||||
json=body,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as resp:
|
||||
if resp.ok:
|
||||
return await resp.json()
|
||||
detail = await resp.text()
|
||||
raise HTTPException(status_code=resp.status, detail=detail)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.debug(f'Failed to refresh terminals: {e}')
|
||||
raise HTTPException(status_code=400, detail='Failed to refresh terminals')
|
||||
|
||||
|
||||
@router.post('/tool_servers/verify')
|
||||
async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)):
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import type { Banner } from '$lib/types';
|
||||
|
||||
export const importConfig = async (token: string, config) => {
|
||||
export const importConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/import`, {
|
||||
@@ -275,7 +275,8 @@ export const putOrchestratorPolicy = async (
|
||||
url: string,
|
||||
key: string,
|
||||
policyId: string,
|
||||
policyData: object
|
||||
policyData: object,
|
||||
authType: string = 'bearer'
|
||||
): Promise<object | null> => {
|
||||
let error = null;
|
||||
|
||||
@@ -288,6 +289,7 @@ export const putOrchestratorPolicy = async (
|
||||
body: JSON.stringify({
|
||||
url: url.replace(/\/$/, ''),
|
||||
key,
|
||||
auth_type: authType,
|
||||
policy_id: policyId,
|
||||
policy_data: policyData
|
||||
})
|
||||
@@ -309,6 +311,91 @@ export const putOrchestratorPolicy = async (
|
||||
return res;
|
||||
};
|
||||
|
||||
export const putOrchestratorLifecycle = async (
|
||||
token: string,
|
||||
url: string,
|
||||
key: string,
|
||||
policyId: string,
|
||||
lifecycleData: object,
|
||||
authType: string = 'bearer'
|
||||
): Promise<object | null> => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/lifecycle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url.replace(/\/$/, ''),
|
||||
key,
|
||||
auth_type: authType,
|
||||
policy_id: policyId,
|
||||
lifecycle_data: lifecycleData
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const refreshOrchestratorTerminals = async (
|
||||
token: string,
|
||||
url: string,
|
||||
key: string,
|
||||
body: {
|
||||
user_id?: string;
|
||||
policy_id?: string;
|
||||
only_idle?: boolean;
|
||||
reset?: boolean;
|
||||
},
|
||||
authType: string = 'bearer'
|
||||
): Promise<object | null> => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url.replace(/\/$/, ''),
|
||||
key,
|
||||
auth_type: authType,
|
||||
...body
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify a terminal server connection via the backend proxy.
|
||||
* Used for system/admin connections to avoid CORS issues and API key exposure.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
import {
|
||||
detectTerminalServerType,
|
||||
verifyTerminalServerConnection,
|
||||
putOrchestratorPolicy
|
||||
putOrchestratorPolicy,
|
||||
putOrchestratorLifecycle,
|
||||
refreshOrchestratorTerminals
|
||||
} from '$lib/apis/configs';
|
||||
import { getTerminalConfig } from '$lib/apis/terminal';
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
export let direct = false;
|
||||
export let connection = null;
|
||||
export let connection: any = null;
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: () => void = () => {};
|
||||
@@ -42,6 +44,7 @@
|
||||
// Policy / auto-detect state
|
||||
let serverType: 'orchestrator' | 'terminal' | null = null;
|
||||
let verifying = false;
|
||||
let refreshing = false;
|
||||
let policyId = '';
|
||||
let policyImage = '';
|
||||
let policyEnvPairs: { key: string; value: string }[] = [];
|
||||
@@ -50,6 +53,13 @@
|
||||
let policyStorage = 'ephemeral';
|
||||
let policyStorageSize = '5Gi';
|
||||
let policyIdleTimeout = 30;
|
||||
let lifecycleJson = '{}';
|
||||
let refreshOnlyIdle = true;
|
||||
let refreshReset = false;
|
||||
|
||||
const stringifyJson = (value: object | null | undefined) => {
|
||||
return JSON.stringify(value && Object.keys(value).length ? value : {}, null, 2);
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
if (connection) {
|
||||
@@ -79,6 +89,9 @@
|
||||
// Restore resources
|
||||
policyCpu = p.cpu_limit ?? '1';
|
||||
policyMemory = p.memory_limit ?? '1Gi';
|
||||
lifecycleJson = stringifyJson(connection?.lifecycle);
|
||||
refreshOnlyIdle = true;
|
||||
refreshReset = false;
|
||||
} else {
|
||||
id = '';
|
||||
url = '';
|
||||
@@ -98,6 +111,9 @@
|
||||
policyStorage = 'ephemeral';
|
||||
policyStorageSize = '5Gi';
|
||||
policyIdleTimeout = 30;
|
||||
lifecycleJson = '{}';
|
||||
refreshOnlyIdle = true;
|
||||
refreshReset = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,6 +178,20 @@
|
||||
}
|
||||
};
|
||||
|
||||
const parseJson = (label: string, value: string): object | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(value || '{}');
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
||||
toast.error($i18n.t('{{label}} must be a JSON object', { label }));
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
toast.error($i18n.t('{{label}} contains invalid JSON', { label }));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildPolicyData = (): object => {
|
||||
const data: Record<string, any> = {};
|
||||
|
||||
@@ -191,6 +221,37 @@
|
||||
return data;
|
||||
};
|
||||
|
||||
const refreshHandler = async () => {
|
||||
if (!policyId) {
|
||||
toast.error($i18n.t('Policy ID is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
refreshing = true;
|
||||
try {
|
||||
const result = await refreshOrchestratorTerminals(
|
||||
localStorage.token,
|
||||
url,
|
||||
key,
|
||||
{
|
||||
policy_id: policyId,
|
||||
only_idle: refreshOnlyIdle,
|
||||
reset: refreshReset
|
||||
},
|
||||
auth_type
|
||||
);
|
||||
toast.success(
|
||||
$i18n.t('Refresh requested: {{count}} terminal(s)', {
|
||||
count: (result as { refreshed?: number } | null)?.refreshed ?? 0
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error($i18n.t('Failed to refresh terminals: {{error}}', { error: err }));
|
||||
} finally {
|
||||
refreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (url === '') {
|
||||
toast.error($i18n.t('Please enter a valid URL'));
|
||||
@@ -201,9 +262,24 @@
|
||||
url = url.replace(/\/$/, '');
|
||||
|
||||
// Save policy to orchestrator if applicable
|
||||
let policyData = {};
|
||||
let lifecycleData = {};
|
||||
if (serverType === 'orchestrator' && !direct && policyId) {
|
||||
const parsedLifecycle = parseJson('Lifecycle JSON', lifecycleJson);
|
||||
if (!parsedLifecycle) return;
|
||||
policyData = buildPolicyData();
|
||||
lifecycleData = parsedLifecycle;
|
||||
|
||||
try {
|
||||
await putOrchestratorPolicy(localStorage.token, url, key, policyId, buildPolicyData());
|
||||
await putOrchestratorPolicy(localStorage.token, url, key, policyId, policyData, auth_type);
|
||||
await putOrchestratorLifecycle(
|
||||
localStorage.token,
|
||||
url,
|
||||
key,
|
||||
policyId,
|
||||
lifecycleData,
|
||||
auth_type
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error($i18n.t('Failed to save policy: {{error}}', { error: err }));
|
||||
return;
|
||||
@@ -224,7 +300,7 @@
|
||||
// Policy fields
|
||||
...(serverType ? { server_type: serverType } : {}),
|
||||
...(serverType === 'orchestrator' && policyId ? { policy_id: policyId } : {}),
|
||||
...(serverType === 'orchestrator' ? { policy: buildPolicyData() } : {})
|
||||
...(serverType === 'orchestrator' ? { policy: policyData, lifecycle: lifecycleData } : {})
|
||||
};
|
||||
|
||||
onSubmit(result);
|
||||
@@ -566,6 +642,46 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t('Lifecycle JSON')}
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
id="lifecycle-json"
|
||||
class={`w-full min-h-24 resize-y text-xs bg-transparent font-mono rounded-lg p-2 border border-gray-200 dark:border-gray-800 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
bind:value={lifecycleJson}
|
||||
spellcheck="false"
|
||||
placeholder={`{\n "reset": {\n "schedule": "@weekly",\n "timezone": "UTC"\n }\n}`}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 mt-2">
|
||||
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<label class="flex items-center gap-1.5">
|
||||
<input type="checkbox" bind:checked={refreshOnlyIdle} />
|
||||
<span>{$i18n.t('Idle only')}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5">
|
||||
<input type="checkbox" bind:checked={refreshReset} />
|
||||
<span>{$i18n.t('Reset persisted files')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition"
|
||||
disabled={refreshing}
|
||||
on:click={refreshHandler}
|
||||
>
|
||||
{refreshing ? $i18n.t('Refreshing...') : $i18n.t('Refresh Terminals')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user