mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat: [wip] Open WebUI manipulation
This commit is contained in:
parent
b6e6e10328
commit
b3ef977484
34
Dockerfile
34
Dockerfile
|
|
@ -1,7 +1,37 @@
|
|||
FROM node:22.16.0-alpine3.22 AS base
|
||||
FROM node:22-slim AS base
|
||||
|
||||
# Install bash & curl for entrypoint script compatibility
|
||||
RUN apk add --no-cache bash curl
|
||||
# as well as dependencies for Playwright Chromium
|
||||
RUN apt-get update && apt-get install -y \
|
||||
bash \
|
||||
curl \
|
||||
wget \
|
||||
ca-certificates \
|
||||
fonts-liberation \
|
||||
libnss3 \
|
||||
libatk-bridge2.0-0 \
|
||||
libdrm2 \
|
||||
libxkbcommon0 \
|
||||
libgbm1 \
|
||||
libasound2 \
|
||||
libxcb-shm0 \
|
||||
libx11-xcb1 \
|
||||
libxrandr2 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libgtk-3-0t64 \
|
||||
libpangocairo-1.0-0 \
|
||||
libpango-1.0-0 \
|
||||
libatk1.0-0t64 \
|
||||
libcairo-gobject2 \
|
||||
libcairo2 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libxrender1 \
|
||||
libasound2t64
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# All deps stage
|
||||
FROM base AS deps
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { MapService } from '#services/map_service';
|
||||
import { OpenWebUIService } from '#services/openwebui_service';
|
||||
import { SystemService } from '#services/system_service';
|
||||
import { inject } from '@adonisjs/core';
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
|
|
@ -7,7 +8,8 @@ import type { HttpContext } from '@adonisjs/core/http'
|
|||
export default class SettingsController {
|
||||
constructor(
|
||||
private systemService: SystemService,
|
||||
private mapService: MapService
|
||||
private mapService: MapService,
|
||||
private openWebUIService: OpenWebUIService
|
||||
) { }
|
||||
|
||||
async system({ inertia }: HttpContext) {
|
||||
|
|
@ -43,6 +45,15 @@ export default class SettingsController {
|
|||
});
|
||||
}
|
||||
|
||||
async models({ inertia }: HttpContext) {
|
||||
const installedModels = await this.openWebUIService.getInstalledModels();
|
||||
return inertia.render('settings/models', {
|
||||
models: {
|
||||
installedModels: installedModels || []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async update({ inertia }: HttpContext) {
|
||||
const updateInfo = await this.systemService.checkLatestVersion();
|
||||
return inertia.render('settings/update', {
|
||||
|
|
|
|||
123
admin/app/services/openwebui_service.ts
Normal file
123
admin/app/services/openwebui_service.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { inject } from '@adonisjs/core'
|
||||
import { chromium } from 'playwright'
|
||||
import { SystemService } from './system_service.js'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
import { DockerService } from './docker_service.js'
|
||||
import { ServiceSlim } from '../../types/services.js'
|
||||
import axios from 'axios'
|
||||
|
||||
@inject()
|
||||
export class OpenWebUIService {
|
||||
constructor(private systemService: SystemService) {}
|
||||
|
||||
async getOpenWebUIToken(): Promise<{
|
||||
token: string
|
||||
location: string
|
||||
} | null> {
|
||||
try {
|
||||
const { openWebUIService } = await this.getOpenWebUIAndOllamaServices()
|
||||
if (!openWebUIService) {
|
||||
logger.warn('[OpenWebUIService] Open WebUI service is not installed.')
|
||||
return null
|
||||
}
|
||||
|
||||
const location = this.extractOpenWebUIUrl(openWebUIService)
|
||||
if (!location) {
|
||||
logger.warn('[OpenWebUIService] Could not determine Open WebUI URL.')
|
||||
return null
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const context = await browser.newContext()
|
||||
const page = await context.newPage()
|
||||
|
||||
try {
|
||||
await page.goto(location, { waitUntil: 'networkidle' })
|
||||
|
||||
const cookies = await context.cookies()
|
||||
const tokenCookie = cookies.find((cookie) => cookie.name === 'token')
|
||||
if (tokenCookie) {
|
||||
return { token: tokenCookie.value, location }
|
||||
}
|
||||
|
||||
return null
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[OpenWebUIService] Failed to get Open WebUI token: ${error instanceof Error ? error.message : error}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getInstalledModels(): Promise<string[] | null> {
|
||||
try {
|
||||
const tokenData = await this.getOpenWebUIToken()
|
||||
if (!tokenData) {
|
||||
logger.warn('[OpenWebUIService] Cannot get installed models without Open WebUI token.')
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await axios.get(tokenData.location + '/ollama/api/tags', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data.models && Array.isArray(response.data.models)) {
|
||||
console.log("GOT RESPONSE DATA:", response.data)
|
||||
return response.data.models as string[]
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`[OpenWebUIService] Unexpected response when fetching installed models: ${response.status}`
|
||||
)
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[OpenWebUIService] Failed to get installed models: ${error instanceof Error ? error.message : error}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async getOpenWebUIAndOllamaServices(): Promise<{
|
||||
openWebUIService: ServiceSlim | null
|
||||
ollamaService: ServiceSlim | null
|
||||
}> {
|
||||
try {
|
||||
const services = await this.systemService.getServices({ installedOnly: true })
|
||||
|
||||
const owuiContainer = services.find(
|
||||
(service) => service.service_name === DockerService.OPEN_WEBUI_SERVICE_NAME
|
||||
)
|
||||
const ollamaContainer = services.find(
|
||||
(service) => service.service_name === DockerService.OLLAMA_SERVICE_NAME
|
||||
)
|
||||
|
||||
return {
|
||||
openWebUIService: owuiContainer || null,
|
||||
ollamaService: ollamaContainer || null,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[OpenWebUIService] Failed to get Open WebUI and Ollama services: ${error instanceof Error ? error.message : error}`
|
||||
)
|
||||
return {
|
||||
openWebUIService: null,
|
||||
ollamaService: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractOpenWebUIUrl(service: ServiceSlim): string | null {
|
||||
const location = service.ui_location || '3000'
|
||||
if (!location || isNaN(Number(location))) {
|
||||
logger.warn(`[OpenWebUIService] Invalid Open WebUI location: ${location}`)
|
||||
return null
|
||||
}
|
||||
return `http://localhost:${location}`
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,13 @@ import {
|
|||
FolderIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { IconArrowBigUpLines, IconDashboard, IconGavel, IconMapRoute } from '@tabler/icons-react'
|
||||
import {
|
||||
IconArrowBigUpLines,
|
||||
IconDashboard,
|
||||
IconDatabaseStar,
|
||||
IconGavel,
|
||||
IconMapRoute,
|
||||
} from '@tabler/icons-react'
|
||||
import StyledSidebar from '~/components/StyledSidebar'
|
||||
import { getServiceLink } from '~/lib/navigation'
|
||||
|
||||
|
|
@ -12,6 +18,7 @@ const navigation = [
|
|||
{ name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false },
|
||||
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
|
||||
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
|
||||
{ name: 'Models Manager', href: '/settings/models', icon: IconDatabaseStar, current: false },
|
||||
{
|
||||
name: 'Service Logs & Metrics',
|
||||
href: getServiceLink('9999'),
|
||||
|
|
|
|||
66
admin/inertia/pages/settings/models.tsx
Normal file
66
admin/inertia/pages/settings/models.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Head } from '@inertiajs/react'
|
||||
import StyledTable from '~/components/StyledTable'
|
||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||
import { ServiceSlim } from '../../../types/services'
|
||||
import { getServiceLink } from '~/lib/navigation'
|
||||
import LoadingSpinner from '~/components/LoadingSpinner'
|
||||
import { IconCheck } from '@tabler/icons-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function ModelsPage(props: { models: { installedModels: string[] } }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Head title="App Settings" />
|
||||
<div className="xl:pl-72 w-full">
|
||||
<main className="px-12 py-6">
|
||||
<h1 className="text-4xl font-semibold mb-4">Models</h1>
|
||||
<p className="text-gray-500 mb-4">Easily manage the AI models available for Open WebUI</p>
|
||||
{loading && <LoadingSpinner fullscreen />}
|
||||
{!loading && (
|
||||
<StyledTable<ServiceSlim & { actions?: any }>
|
||||
className="font-semibold"
|
||||
rowLines={true}
|
||||
columns={[
|
||||
{
|
||||
accessor: 'friendly_name',
|
||||
title: 'Name',
|
||||
render(record) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p>{record.friendly_name || record.service_name}</p>
|
||||
<p className="text-sm text-gray-500">{record.description}</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: 'ui_location',
|
||||
title: 'Port',
|
||||
render: (record) => (
|
||||
<a
|
||||
href={getServiceLink(record.ui_location || 'unknown')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-desert-green hover:underline font-semibold"
|
||||
>
|
||||
{record.ui_location}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'installed',
|
||||
title: 'Installed',
|
||||
render: (record) =>
|
||||
record.installed ? <IconCheck className="h-6 w-6 text-desert-green" /> : '',
|
||||
},
|
||||
]}
|
||||
data={[]}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
45
admin/package-lock.json
generated
45
admin/package-lock.json
generated
|
|
@ -43,6 +43,7 @@
|
|||
"maplibre-gl": "^4.7.1",
|
||||
"mysql2": "^3.14.1",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"playwright": "^1.57.0",
|
||||
"pmtiles": "^4.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.1.0",
|
||||
|
|
@ -9678,6 +9679,50 @@
|
|||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"scripts": {
|
||||
"start": "node bin/server.js",
|
||||
"build": "node ace build",
|
||||
"postinstall": "playwright install chromium --with-deps",
|
||||
"dev": "node ace serve --hmr",
|
||||
"test": "node ace test",
|
||||
"lint": "eslint .",
|
||||
|
|
@ -91,6 +92,7 @@
|
|||
"maplibre-gl": "^4.7.1",
|
||||
"mysql2": "^3.14.1",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"playwright": "^1.57.0",
|
||||
"pmtiles": "^4.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.1.0",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ router
|
|||
router.get('/apps', [SettingsController, 'apps'])
|
||||
router.get('/legal', [SettingsController, 'legal'])
|
||||
router.get('/maps', [SettingsController, 'maps'])
|
||||
router.get('/models', [SettingsController, 'models'])
|
||||
router.get('/update', [SettingsController, 'update'])
|
||||
router.get('/zim', [SettingsController, 'zim'])
|
||||
router.get('/zim/remote-explorer', [SettingsController, 'zimRemote'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user