refac
This commit is contained in:
@@ -57,10 +57,16 @@ class AutomationRun(Base):
|
||||
####################
|
||||
|
||||
|
||||
class AutomationTerminalConfig(BaseModel):
|
||||
server_id: str
|
||||
cwd: Optional[str] = None
|
||||
|
||||
|
||||
class AutomationData(BaseModel):
|
||||
prompt: str
|
||||
model_id: str
|
||||
rrule: str
|
||||
terminal: Optional[AutomationTerminalConfig] = None
|
||||
|
||||
|
||||
class AutomationModel(BaseModel):
|
||||
|
||||
@@ -91,6 +91,15 @@ async def create_new_automation(
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
# Validate terminal server exists if linked
|
||||
if form_data.data.terminal and form_data.data.terminal.server_id:
|
||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||
if not any(c.get('id') == form_data.data.terminal.server_id for c in connections):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Terminal server not found',
|
||||
)
|
||||
|
||||
tz = user.timezone
|
||||
automation = Automations.insert(
|
||||
user.id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db
|
||||
@@ -139,6 +148,15 @@ async def update_automation_by_id(
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
# Validate terminal server exists if linked
|
||||
if form_data.data.terminal and form_data.data.terminal.server_id:
|
||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||
if not any(c.get('id') == form_data.data.terminal.server_id for c in connections):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Terminal server not found',
|
||||
)
|
||||
|
||||
tz = user.timezone
|
||||
updated = Automations.update_by_id(
|
||||
id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db
|
||||
|
||||
@@ -211,6 +211,66 @@ def _resolve_model_filter_ids(app, model_id: str) -> list[str]:
|
||||
return list(filter_ids) if filter_ids else []
|
||||
|
||||
|
||||
async def _set_terminal_cwd(
|
||||
app, server_id: str, user, cwd: str, chat_id: str
|
||||
) -> None:
|
||||
"""Set the working directory on a terminal server via the proxy.
|
||||
|
||||
Routes through the open-webui terminal proxy endpoint so that
|
||||
auth headers, orchestrator policy routing, and X-User-Id are
|
||||
handled correctly — same path the frontend uses.
|
||||
"""
|
||||
import aiohttp
|
||||
|
||||
connections = getattr(
|
||||
getattr(app, 'state', None), 'config', None
|
||||
)
|
||||
if connections is None:
|
||||
return
|
||||
connections = getattr(connections, 'TERMINAL_SERVER_CONNECTIONS', None) or []
|
||||
connection = next((c for c in connections if c.get('id') == server_id), None)
|
||||
if connection is None:
|
||||
log.warning(f'Terminal server {server_id} not found for CWD set')
|
||||
return
|
||||
|
||||
base_url = (connection.get('url') or '').rstrip('/')
|
||||
if not base_url:
|
||||
return
|
||||
|
||||
# Build target URL — route through orchestrator policy if configured
|
||||
policy_id = connection.get('policy_id')
|
||||
if connection.get('server_type') == 'orchestrator' and policy_id:
|
||||
target_url = f'{base_url}/p/{policy_id}/files/cwd'
|
||||
else:
|
||||
target_url = f'{base_url}/files/cwd'
|
||||
|
||||
headers = {'Content-Type': 'application/json', 'X-User-Id': user.id}
|
||||
if chat_id:
|
||||
headers['X-Session-Id'] = chat_id
|
||||
|
||||
auth_type = connection.get('auth_type', 'bearer')
|
||||
if auth_type == 'bearer':
|
||||
headers['Authorization'] = f'Bearer {connection.get("key", "")}'
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=10)
|
||||
) as session:
|
||||
async with session.post(
|
||||
target_url,
|
||||
json={'path': cwd},
|
||||
headers=headers,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
body = await resp.text()
|
||||
log.warning(
|
||||
f'Failed to set terminal CWD to {cwd}: '
|
||||
f'HTTP {resp.status} — {body[:200]}'
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f'Failed to set terminal CWD: {e}')
|
||||
|
||||
|
||||
async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
"""Execute an automation through the full chat completion pipeline.
|
||||
|
||||
@@ -226,6 +286,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
|
||||
prompt = prompt_template(automation.data["prompt"], user)
|
||||
model_id = automation.data["model_id"]
|
||||
terminal_config = automation.data.get("terminal")
|
||||
|
||||
# Generate proper UUIDs for messages (same as frontend)
|
||||
user_msg_id = str(uuid4())
|
||||
@@ -291,6 +352,14 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
features = _resolve_model_features(app, model_id)
|
||||
filter_ids = _resolve_model_filter_ids(app, model_id)
|
||||
|
||||
# If a terminal is linked, set the CWD before building the payload
|
||||
terminal_id = None
|
||||
if terminal_config and terminal_config.get("server_id"):
|
||||
terminal_id = terminal_config["server_id"]
|
||||
cwd = terminal_config.get("cwd")
|
||||
if cwd:
|
||||
await _set_terminal_cwd(app, terminal_id, user, cwd, chat.id)
|
||||
|
||||
# Build the same payload the frontend sends to /api/chat/completions
|
||||
form_data = {
|
||||
"model": model_id,
|
||||
@@ -308,6 +377,8 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
form_data["features"] = features
|
||||
if filter_ids:
|
||||
form_data["filter_ids"] = filter_ids
|
||||
if terminal_id:
|
||||
form_data["terminal_id"] = terminal_id
|
||||
|
||||
# Call the full chat completion pipeline (same as POST /api/chat/completions).
|
||||
# The handler reference is stored on app.state to avoid circular imports.
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export type AutomationTerminalConfig = {
|
||||
server_id: string;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
export type AutomationData = {
|
||||
prompt: string;
|
||||
model_id: string;
|
||||
rrule: string;
|
||||
terminal?: AutomationTerminalConfig;
|
||||
};
|
||||
|
||||
export type AutomationForm = {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import Cloud from '$lib/components/icons/Cloud.svelte';
|
||||
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
type AutomationForm,
|
||||
type AutomationResponse
|
||||
} from '$lib/apis/automations';
|
||||
import { getTerminalServers, type TerminalServer } from '$lib/apis/terminal/index';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -42,9 +43,19 @@
|
||||
let loading = false;
|
||||
let showScheduleDropdown = false;
|
||||
let showModelDropdown = false;
|
||||
let showTerminalDropdown = false;
|
||||
let modelSearch = '';
|
||||
let customRrule = '';
|
||||
|
||||
// Terminal state
|
||||
let terminalServers: TerminalServer[] = [];
|
||||
let terminalServerId = '';
|
||||
let terminalCwd = '';
|
||||
|
||||
$: terminalLabel = terminalServerId
|
||||
? terminalServers.find((s) => s.id === terminalServerId)?.name || 'Terminal'
|
||||
: $i18n.t('Terminal');
|
||||
|
||||
$: modelLabel = model_id
|
||||
? $models.find((m) => m.id === model_id)?.name || model_id
|
||||
: $i18n.t('Select model');
|
||||
@@ -198,7 +209,15 @@
|
||||
data: {
|
||||
prompt: prompt.trim(),
|
||||
model_id: model_id.trim(),
|
||||
rrule: buildRrule()
|
||||
rrule: buildRrule(),
|
||||
...(terminalServerId
|
||||
? {
|
||||
terminal: {
|
||||
server_id: terminalServerId,
|
||||
...(terminalCwd.trim() ? { cwd: terminalCwd.trim() } : {})
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
is_active
|
||||
};
|
||||
@@ -219,13 +238,22 @@
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
const init = async () => {
|
||||
// Load terminal servers
|
||||
try {
|
||||
terminalServers = await getTerminalServers(localStorage.token);
|
||||
} catch {
|
||||
terminalServers = [];
|
||||
}
|
||||
|
||||
if (automation) {
|
||||
name = automation.name;
|
||||
prompt = automation.data.prompt;
|
||||
model_id = automation.data.model_id;
|
||||
is_active = automation.is_active;
|
||||
parseRrule(automation.data.rrule);
|
||||
terminalServerId = automation.data.terminal?.server_id || '';
|
||||
terminalCwd = automation.data.terminal?.cwd || '';
|
||||
} else {
|
||||
name = '';
|
||||
prompt = '';
|
||||
@@ -239,8 +267,11 @@
|
||||
minute = 0;
|
||||
selectedDays = [];
|
||||
monthDay = 1;
|
||||
terminalServerId = '';
|
||||
terminalCwd = '';
|
||||
}
|
||||
showScheduleDropdown = false;
|
||||
showTerminalDropdown = false;
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
@@ -513,6 +544,99 @@
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
<!-- Terminal dropdown -->
|
||||
{#if terminalServers.length > 0}
|
||||
<Dropdown bind:show={showTerminalDropdown} side="top" align="start">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-2xl text-xs transition
|
||||
{terminalServerId
|
||||
? 'text-black dark:text-gray-100'
|
||||
: 'text-gray-600 dark:text-gray-400'}
|
||||
hover:bg-black/5 dark:hover:bg-white/5"
|
||||
>
|
||||
<Cloud className="size-3.5 shrink-0" strokeWidth="2" />
|
||||
<span class="whitespace-nowrap max-w-32 truncate">{terminalLabel}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="size-2.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
slot="content"
|
||||
class="rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 min-w-56 max-w-56 p-1"
|
||||
>
|
||||
<div class="px-2 text-xs text-gray-500 py-1">
|
||||
{$i18n.t('Terminal')}
|
||||
</div>
|
||||
|
||||
{#each terminalServers as server (server.id)}
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl {terminalServerId ===
|
||||
server.id
|
||||
? 'bg-gray-50 dark:bg-gray-800/50'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (terminalServerId === server.id) {
|
||||
terminalServerId = '';
|
||||
terminalCwd = '';
|
||||
} else {
|
||||
terminalServerId = server.id;
|
||||
}
|
||||
showTerminalDropdown = false;
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-1 gap-2 items-center truncate">
|
||||
<Cloud className="size-4 shrink-0" strokeWidth="2" />
|
||||
<span class="truncate">{server.name || server.id}</span>
|
||||
</div>
|
||||
{#if terminalServerId === server.id}
|
||||
<div class="shrink-0 text-emerald-600 dark:text-emerald-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if terminalServerId}
|
||||
<div class="border-t border-gray-100 dark:border-gray-800 mt-1 pt-1">
|
||||
<div class="px-2.5 py-1 text-xs text-gray-500">
|
||||
{$i18n.t('Working Directory')}
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={terminalCwd}
|
||||
placeholder="/home/user/project"
|
||||
class="w-full bg-transparent outline-hidden text-xs py-1.5 placeholder:text-gray-400 dark:placeholder:text-gray-600"
|
||||
on:click={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user