mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
remove the comfyui frontend, clean up koboldcpp's terminal spam to be more meaningful, start proxying kcpp APIs through 127.0.0.1:5002
This commit is contained in:
parent
0c869ea029
commit
f2d15591a7
19 changed files with 364 additions and 899 deletions
4
.github/workflows/aur-release.yml
vendored
4
.github/workflows/aur-release.yml
vendored
|
|
@ -161,7 +161,7 @@ jobs:
|
|||
license=('AGPL-3.0-or-later')
|
||||
depends=('gtk3' 'nss')
|
||||
optdepends=('nodejs: Required for SillyTavern integration'
|
||||
'uv: Required for OpenWebUI and ComfyUI integrations')
|
||||
'uv: Required for OpenWebUI integration')
|
||||
provides=('gerbil')
|
||||
conflicts=('gerbil-git')
|
||||
source=("gerbil-${pkgver}.AppImage::${{ steps.release_info.outputs.appimage_url }}"
|
||||
|
|
@ -298,7 +298,7 @@ jobs:
|
|||
depends = gtk3
|
||||
depends = nss
|
||||
optdepends = nodejs: Required for SillyTavern integration
|
||||
optdepends = uv: Required for OpenWebUI and ComfyUI integrations
|
||||
optdepends = uv: Required for OpenWebUI integration
|
||||
provides = gerbil
|
||||
conflicts = gerbil-git
|
||||
source = gerbil-${{ steps.release_info.outputs.version }}.AppImage::${{ steps.release_info.outputs.appimage_url }}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"productName": "Gerbil",
|
||||
"version": "1.8.5",
|
||||
"version": "1.9.0",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
|
|||
|
|
@ -6,16 +6,11 @@ import { getServerInterfaceInfo } from '@/utils/interface';
|
|||
import { TITLEBAR_HEIGHT, STATUSBAR_HEIGHT } from '@/constants';
|
||||
|
||||
interface ServerTabProps {
|
||||
serverUrl?: string;
|
||||
isServerReady?: boolean;
|
||||
activeTab?: string;
|
||||
}
|
||||
|
||||
export const ServerTab = ({
|
||||
serverUrl,
|
||||
isServerReady,
|
||||
activeTab,
|
||||
}: ServerTabProps) => {
|
||||
export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
|
||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||
const { frontendPreference, imageGenerationFrontendPreference } =
|
||||
usePreferencesStore();
|
||||
|
|
@ -33,17 +28,11 @@ export const ServerTab = ({
|
|||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
isImageGenerationMode: effectiveImageMode,
|
||||
serverUrl: serverUrl || '',
|
||||
}),
|
||||
[
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
effectiveImageMode,
|
||||
serverUrl,
|
||||
]
|
||||
[frontendPreference, imageGenerationFrontendPreference, effectiveImageMode]
|
||||
);
|
||||
|
||||
if (!isServerReady || !serverUrl) {
|
||||
if (!isServerReady) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
|
|
@ -60,10 +49,7 @@ export const ServerTab = ({
|
|||
</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
The{' '}
|
||||
{title.toLowerCase().includes('ui') ||
|
||||
title.toLowerCase().includes('comfy')
|
||||
? 'image generation'
|
||||
: 'chat'}{' '}
|
||||
{title.toLowerCase().includes('ui') ? 'image generation' : 'chat'}{' '}
|
||||
interface will load automatically when ready
|
||||
</Text>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -95,8 +95,6 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
|
|||
signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN;
|
||||
} else if (effectiveFrontend === 'openwebui') {
|
||||
signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI;
|
||||
} else if (effectiveFrontend === 'comfyui') {
|
||||
signalToCheck = SERVER_READY_SIGNALS.COMFYUI;
|
||||
}
|
||||
|
||||
if (newData.includes(signalToCheck)) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export const InterfaceScreen = ({
|
|||
activeTab,
|
||||
onTabChange,
|
||||
}: InterfaceScreenProps) => {
|
||||
const [serverUrl, setServerUrl] = useState('');
|
||||
const [isServerReady, setIsServerReady] = useState(false);
|
||||
const terminalTabRef = useRef<TerminalTabRef>(null);
|
||||
|
||||
|
|
@ -42,16 +41,13 @@ export const InterfaceScreen = ({
|
|||
]
|
||||
);
|
||||
|
||||
const handleServerReady = useCallback(
|
||||
(url: string) => {
|
||||
setServerUrl(url);
|
||||
setIsServerReady(true);
|
||||
if (onTabChange) {
|
||||
onTabChange(defaultInterfaceTab);
|
||||
}
|
||||
},
|
||||
[onTabChange, defaultInterfaceTab]
|
||||
);
|
||||
const handleServerReady = useCallback(() => {
|
||||
setIsServerReady(true);
|
||||
|
||||
if (onTabChange) {
|
||||
onTabChange(defaultInterfaceTab);
|
||||
}
|
||||
}, [onTabChange, defaultInterfaceTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'terminal' && terminalTabRef.current) {
|
||||
|
|
@ -78,7 +74,6 @@ export const InterfaceScreen = ({
|
|||
}}
|
||||
>
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
activeTab={activeTab || undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ interface FrontendRequirement {
|
|||
interface FrontendConfig {
|
||||
value: string;
|
||||
label: string;
|
||||
badges: string[];
|
||||
requirements?: FrontendRequirement[];
|
||||
requirementCheck?: () => Promise<boolean>;
|
||||
}
|
||||
|
|
@ -46,12 +45,10 @@ export const FrontendInterfaceSelector = ({
|
|||
{
|
||||
value: 'koboldcpp',
|
||||
label: 'Built-in',
|
||||
badges: ['Text', 'Image'],
|
||||
},
|
||||
{
|
||||
value: 'sillytavern',
|
||||
label: FRONTENDS.SILLYTAVERN,
|
||||
badges: ['Text', 'Image'],
|
||||
requirements: [
|
||||
{
|
||||
id: 'nodejs',
|
||||
|
|
@ -65,20 +62,6 @@ export const FrontendInterfaceSelector = ({
|
|||
{
|
||||
value: 'openwebui',
|
||||
label: FRONTENDS.OPENWEBUI,
|
||||
badges: ['Text', 'Image'],
|
||||
requirements: [
|
||||
{
|
||||
id: 'uv',
|
||||
name: 'uv',
|
||||
url: 'https://docs.astral.sh/uv/getting-started/installation/',
|
||||
},
|
||||
],
|
||||
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
|
||||
},
|
||||
{
|
||||
value: 'comfyui',
|
||||
label: FRONTENDS.COMFYUI,
|
||||
badges: ['Image'],
|
||||
requirements: [
|
||||
{
|
||||
id: 'uv',
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export const SERVER_READY_SIGNALS = {
|
|||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||
SILLYTAVERN: 'SillyTavern is listening on',
|
||||
OPENWEBUI: 'Waiting for application startup.',
|
||||
COMFYUI: 'Starting server',
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_CONTEXT_SIZE = 4096;
|
||||
|
|
@ -24,24 +23,17 @@ export const SILLYTAVERN = {
|
|||
PORT: 3000,
|
||||
PROXY_PORT: 3001,
|
||||
get URL() {
|
||||
return `http://localhost:${this.PORT}`;
|
||||
return `http://127.0.0.1:${this.PORT}`;
|
||||
},
|
||||
get PROXY_URL() {
|
||||
return `http://localhost:${this.PROXY_PORT}`;
|
||||
return `http://127.0.0.1:${this.PROXY_PORT}`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const OPENWEBUI = {
|
||||
PORT: 8080,
|
||||
get URL() {
|
||||
return `http://localhost:${this.PORT}`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const COMFYUI = {
|
||||
PORT: 8188,
|
||||
get URL() {
|
||||
return `http://localhost:${this.PORT}`;
|
||||
return `http://127.0.0.1:${this.PORT}`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
@ -51,7 +43,6 @@ export const GITHUB_API = {
|
|||
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
||||
GERBIL_REPO: 'lone-cloud/gerbil',
|
||||
COMFYUI_REPO: 'comfyanonymous/ComfyUI',
|
||||
get LATEST_RELEASE_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest` as const;
|
||||
},
|
||||
|
|
@ -61,12 +52,6 @@ export const GITHUB_API = {
|
|||
get ROCM_LATEST_RELEASE_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest` as const;
|
||||
},
|
||||
get COMFYUI_DOWNLOAD_URL() {
|
||||
return `${this.GITHUB_BASE_URL}/${this.COMFYUI_REPO}/archive/refs/heads/master.zip` as const;
|
||||
},
|
||||
get COMFYUI_LATEST_COMMIT_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.COMFYUI_REPO}/commits/master` as const;
|
||||
},
|
||||
get GERBIL_GITHUB_URL() {
|
||||
return `${this.GITHUB_BASE_URL}/${this.GERBIL_REPO}` as const;
|
||||
},
|
||||
|
|
@ -94,7 +79,6 @@ export const FRONTENDS = {
|
|||
STABLE_UI: 'Stable UI',
|
||||
SILLYTAVERN: 'SillyTavern',
|
||||
OPENWEBUI: 'Open WebUI',
|
||||
COMFYUI: 'ComfyUI',
|
||||
} as const;
|
||||
|
||||
export const ZOOM = {
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
export const KLITE_CSS_OVERRIDE = `
|
||||
<style id="gerbil-css-override">
|
||||
* {
|
||||
transition: 100ms ease all;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.adaptivecontainer {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#lastreq1 {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
#inputrow {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#navbarNavDropdown {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#actionmenuitems {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#inputrow > :nth-child(1) {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
#inputrow.show_mode > :nth-child(1) {
|
||||
flex: 0 0 70px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
#inputrow > :nth-child(3) {
|
||||
flex: 0 0 70px;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
#inputrow.show_mode > :nth-child(3) button {
|
||||
background-color: #129c00;
|
||||
font-size: 14px;
|
||||
}
|
||||
#inputrow.show_mode > :nth-child(3) button:hover {
|
||||
background-color: #058105;
|
||||
}
|
||||
</style>`;
|
||||
7
src/constants/proxy.ts
Normal file
7
src/constants/proxy.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const PROXY = {
|
||||
HOST: '127.0.0.1',
|
||||
PORT: 5002,
|
||||
get URL() {
|
||||
return `http://${this.HOST}:${this.PORT}`;
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -16,7 +16,6 @@ import { safeExecute } from '@/utils/node/logging';
|
|||
import { stopKoboldCpp } from '@/main/modules/koboldcpp/launcher';
|
||||
import { stopFrontend as stopSillyTavern } from '@/main/modules/sillytavern';
|
||||
import { stopFrontend as stopOpenWebUI } from '@/main/modules/openwebui';
|
||||
import { stopFrontend as stopComfyUI } from '@/main/modules/comfyui';
|
||||
import { setupIPCHandlers } from '@/main/ipc';
|
||||
import { ensureDir } from '@/utils/node/fs';
|
||||
import { PRODUCT_NAME } from '@/constants';
|
||||
|
|
@ -75,7 +74,6 @@ export async function initializeApp(options?: { startMinimized?: boolean }) {
|
|||
stopKoboldCpp(),
|
||||
stopSillyTavern(),
|
||||
stopOpenWebUI(),
|
||||
stopComfyUI(),
|
||||
];
|
||||
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
|
|||
import { logError } from '@/utils/node/logging';
|
||||
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
|
||||
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||
import { stopFrontend as stopComfyUIFrontend } from '@/main/modules/comfyui';
|
||||
import {
|
||||
isUvAvailable,
|
||||
isNpxAvailable,
|
||||
|
|
@ -147,7 +146,6 @@ export function setupIPCHandlers() {
|
|||
stopKoboldCpp();
|
||||
stopSillyTavernFrontend();
|
||||
stopOpenWebUIFrontend();
|
||||
stopComfyUIFrontend();
|
||||
});
|
||||
|
||||
ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
|
||||
|
|
|
|||
|
|
@ -1,599 +0,0 @@
|
|||
import { spawn } from 'child_process';
|
||||
import { dirname, join } from 'path';
|
||||
import {
|
||||
access,
|
||||
stat,
|
||||
mkdir,
|
||||
unlink,
|
||||
copyFile,
|
||||
readFile,
|
||||
writeFile,
|
||||
rm,
|
||||
} from 'fs/promises';
|
||||
import { platform, on } from 'process';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import yauzl from 'yauzl';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { app } from 'electron';
|
||||
|
||||
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from './window';
|
||||
import { getInstallDir } from './config';
|
||||
import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants';
|
||||
import { terminateProcess } from '@/utils/node/process';
|
||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||
import { ensureDir } from '@/utils/node/fs';
|
||||
import { detectGPU } from './hardware';
|
||||
import { getUvEnvironment } from './dependencies';
|
||||
|
||||
interface ComfyUIVersionInfo {
|
||||
sha: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
async function getLatestComfyUIVersion() {
|
||||
return safeExecute(async () => {
|
||||
const response = await fetch(GITHUB_API.COMFYUI_LATEST_COMMIT_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch ComfyUI version: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
sha: data.sha,
|
||||
date: data.commit.committer.date,
|
||||
};
|
||||
}, 'Failed to fetch latest ComfyUI version');
|
||||
}
|
||||
|
||||
async function getCurrentComfyUIVersion(workspaceDir: string) {
|
||||
try {
|
||||
const versionFile = join(workspaceDir, '.version.json');
|
||||
const content = await readFile(versionFile, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveComfyUIVersion(
|
||||
workspaceDir: string,
|
||||
version: ComfyUIVersionInfo
|
||||
) {
|
||||
await tryExecute(async () => {
|
||||
const versionFile = join(workspaceDir, '.version.json');
|
||||
await writeFile(versionFile, JSON.stringify(version, null, 2));
|
||||
}, 'Failed to save ComfyUI version info');
|
||||
}
|
||||
|
||||
async function shouldUpdateComfyUI(workspaceDir: string) {
|
||||
const current = await getCurrentComfyUIVersion(workspaceDir);
|
||||
const latest = await getLatestComfyUIVersion();
|
||||
|
||||
if (!current || !latest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return current.sha !== latest.sha;
|
||||
}
|
||||
|
||||
let comfyUIProcess: ChildProcess | null = null;
|
||||
|
||||
function getPythonPath(workspaceDir: string) {
|
||||
const isWindows = platform === 'win32';
|
||||
const pythonExecutable = isWindows ? 'python.exe' : 'python';
|
||||
const scriptsDir = isWindows ? 'Scripts' : 'bin';
|
||||
return join(workspaceDir, '.venv', scriptsDir, pythonExecutable);
|
||||
}
|
||||
|
||||
on('SIGINT', () => {
|
||||
void stopFrontend();
|
||||
});
|
||||
|
||||
on('SIGTERM', () => {
|
||||
void stopFrontend();
|
||||
});
|
||||
|
||||
async function shouldForceCPUMode() {
|
||||
try {
|
||||
const gpuInfo = await detectGPU();
|
||||
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||
const isWindows = platform === 'win32';
|
||||
|
||||
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function getPyTorchInstallArgs(pythonPath: string) {
|
||||
const args = [
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
pythonPath,
|
||||
'torch',
|
||||
'torchvision',
|
||||
'torchaudio',
|
||||
];
|
||||
|
||||
try {
|
||||
const gpuInfo = await detectGPU();
|
||||
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||
const isWindows = platform === 'win32';
|
||||
|
||||
if (hasAMD && !isWindows) {
|
||||
sendKoboldOutput(
|
||||
'AMD GPU detected, installing PyTorch with ROCm support...'
|
||||
);
|
||||
args.push('--index-url', 'https://download.pytorch.org/whl/rocm6.4');
|
||||
} else if (hasNVIDIA) {
|
||||
sendKoboldOutput(
|
||||
'NVIDIA GPU detected, installing PyTorch with CUDA support...'
|
||||
);
|
||||
args.push('--extra-index-url', 'https://download.pytorch.org/whl/cu121');
|
||||
} else {
|
||||
if (hasAMD && isWindows) {
|
||||
sendKoboldOutput(
|
||||
'AMD GPU detected on Windows, installing CPU PyTorch (ROCm not available on Windows)...'
|
||||
);
|
||||
} else {
|
||||
sendKoboldOutput(
|
||||
'No dedicated GPU detected, installing CPU-only PyTorch...'
|
||||
);
|
||||
}
|
||||
args.push('--index-url', 'https://download.pytorch.org/whl/cpu');
|
||||
}
|
||||
} catch {
|
||||
sendKoboldOutput('Could not detect GPU, installing default PyTorch...');
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
async function downloadAndExtractComfyUI(workspaceDir: string) {
|
||||
sendKoboldOutput('Downloading ComfyUI...');
|
||||
|
||||
const response = await fetch(GITHUB_API.COMFYUI_DOWNLOAD_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ComfyUI: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const zipPath = join(workspaceDir, 'comfyui.zip');
|
||||
const stream = createWriteStream(zipPath);
|
||||
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
const pump = async (): Promise<void> => {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
stream.end();
|
||||
return;
|
||||
}
|
||||
stream.write(Buffer.from(value));
|
||||
return pump();
|
||||
};
|
||||
await pump();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.on('finish', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const zipStats = await stat(zipPath);
|
||||
sendKoboldOutput(
|
||||
`ComfyUI downloaded (${Math.round(zipStats.size / 1024 / 1024)}MB), extracting...`
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile!.readEntry();
|
||||
zipfile!.on('entry', (entry) => {
|
||||
const entryPath = entry.fileName;
|
||||
const destPath = join(workspaceDir, entryPath.replace(/^[^/]+\//, ''));
|
||||
|
||||
if (/\/$/.test(entryPath)) {
|
||||
mkdir(destPath, { recursive: true })
|
||||
.then(() => zipfile!.readEntry())
|
||||
.catch(reject);
|
||||
} else {
|
||||
zipfile!.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
mkdir(dirname(destPath), { recursive: true })
|
||||
.then(() => {
|
||||
const writeStream = createWriteStream(destPath);
|
||||
readStream!.pipe(writeStream);
|
||||
writeStream.on('close', () => zipfile!.readEntry());
|
||||
writeStream.on('error', reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
zipfile!.on('end', () => {
|
||||
unlink(zipPath)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
sendKoboldOutput('ComfyUI extracted successfully');
|
||||
|
||||
const latestVersion = await getLatestComfyUIVersion();
|
||||
if (latestVersion) {
|
||||
await saveComfyUIVersion(workspaceDir, latestVersion);
|
||||
}
|
||||
}
|
||||
|
||||
async function installDependencies(
|
||||
workspaceDir: string,
|
||||
env: Record<string, string | undefined>
|
||||
) {
|
||||
sendKoboldOutput('Installing ComfyUI dependencies...');
|
||||
|
||||
const requirementsPath = join(workspaceDir, 'requirements.txt');
|
||||
const requirementsExists = await access(requirementsPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!requirementsExists) {
|
||||
throw new Error('requirements.txt not found in ComfyUI directory');
|
||||
}
|
||||
|
||||
const pythonPath = getPythonPath(workspaceDir);
|
||||
const torchInstallArgs = await getPyTorchInstallArgs(pythonPath);
|
||||
|
||||
const torchInstallProcess = spawn('uv', torchInstallArgs, {
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
torchInstallProcess.on('exit', (torchCode: number | null) => {
|
||||
if (torchCode === 0) {
|
||||
sendKoboldOutput(
|
||||
'PyTorch installed successfully, installing remaining dependencies...'
|
||||
);
|
||||
|
||||
const pipInstallProcess = spawn(
|
||||
'uv',
|
||||
[
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
getPythonPath(workspaceDir),
|
||||
'-r',
|
||||
requirementsPath,
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
pipInstallProcess.on('exit', (pipCode: number | null) => {
|
||||
if (pipCode === 0) {
|
||||
sendKoboldOutput('ComfyUI dependencies installed successfully');
|
||||
|
||||
const verifyProcess = spawn(
|
||||
getPythonPath(workspaceDir),
|
||||
[
|
||||
'-c',
|
||||
'import torch; print("PyTorch version:", torch.__version__)',
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
verifyProcess.on('exit', (verifyCode: number | null) => {
|
||||
if (verifyCode === 0) {
|
||||
sendKoboldOutput('ComfyUI installation verified successfully');
|
||||
resolve();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`ComfyUI verification failed with code ${verifyCode}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
verifyProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (verifyProcess.stdout) {
|
||||
verifyProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (verifyProcess.stderr) {
|
||||
verifyProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(
|
||||
`Verify Error: ${data.toString('utf8')}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Dependency installation failed with code ${pipCode}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
pipInstallProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (pipInstallProcess.stdout) {
|
||||
pipInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (pipInstallProcess.stderr) {
|
||||
pipInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`PyTorch installation failed with code ${torchCode}`));
|
||||
}
|
||||
});
|
||||
|
||||
torchInstallProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (torchInstallProcess.stdout) {
|
||||
torchInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (torchInstallProcess.stderr) {
|
||||
torchInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function installComfyUI(workspaceDir: string) {
|
||||
const env = await getUvEnvironment();
|
||||
|
||||
sendKoboldOutput('Creating virtual environment...');
|
||||
|
||||
const installProcess = spawn(
|
||||
'uv',
|
||||
['venv', '--python', '3.11', join(workspaceDir, '.venv')],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
installProcess.on('exit', async (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
await downloadAndExtractComfyUI(workspaceDir);
|
||||
await installDependencies(workspaceDir, env);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Virtual environment creation failed with code ${code}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
installProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (installProcess.stdout) {
|
||||
installProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (installProcess.stderr) {
|
||||
installProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updateComfyUI(workspaceDir: string) {
|
||||
const backupDir = join(workspaceDir, 'backup');
|
||||
await ensureDir(backupDir);
|
||||
|
||||
const filesToBackup = ['main.py', 'requirements.txt'];
|
||||
const backupPromises = filesToBackup.map(async (file) => {
|
||||
const srcPath = join(workspaceDir, file);
|
||||
const destPath = join(backupDir, file);
|
||||
try {
|
||||
await copyFile(srcPath, destPath);
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(backupPromises);
|
||||
|
||||
try {
|
||||
await downloadAndExtractComfyUI(workspaceDir);
|
||||
sendKoboldOutput('ComfyUI updated successfully');
|
||||
|
||||
await rm(backupDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
sendKoboldOutput('Update failed, restoring backup...');
|
||||
|
||||
const restorePromises = filesToBackup.map(async (file) => {
|
||||
const srcPath = join(backupDir, file);
|
||||
const destPath = join(workspaceDir, file);
|
||||
try {
|
||||
await copyFile(srcPath, destPath);
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(restorePromises);
|
||||
await rm(backupDir, { recursive: true, force: true });
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureComfyUIInstalled() {
|
||||
const installDir = getInstallDir();
|
||||
const comfyUIWorkspace = join(installDir, 'comfyui-workspace');
|
||||
|
||||
await ensureDir(comfyUIWorkspace);
|
||||
|
||||
const comfyUIMainPath = join(comfyUIWorkspace, 'main.py');
|
||||
const isComfyUIInstalled = await access(comfyUIMainPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const needsUpdate =
|
||||
isComfyUIInstalled && (await shouldUpdateComfyUI(comfyUIWorkspace));
|
||||
|
||||
if (!isComfyUIInstalled) {
|
||||
sendKoboldOutput('ComfyUI not found, installing...');
|
||||
await installComfyUI(comfyUIWorkspace);
|
||||
} else if (needsUpdate) {
|
||||
sendKoboldOutput('ComfyUI update available, updating...');
|
||||
await updateComfyUI(comfyUIWorkspace);
|
||||
} else {
|
||||
sendKoboldOutput('ComfyUI found in workspace and up to date');
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForComfyUIToStart() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const checkForOutput = (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
if (output.includes(SERVER_READY_SIGNALS.COMFYUI)) {
|
||||
sendKoboldOutput('ComfyUI is now running!');
|
||||
resolve();
|
||||
|
||||
if (comfyUIProcess?.stdout) {
|
||||
comfyUIProcess.stdout.removeListener('data', checkForOutput);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (comfyUIProcess?.stdout) {
|
||||
comfyUIProcess.stdout.on('data', checkForOutput);
|
||||
} else {
|
||||
reject(new Error('ComfyUI process stdout not available'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function startFrontend(args: string[]) {
|
||||
try {
|
||||
const config = {
|
||||
name: 'comfyui',
|
||||
port: COMFYUI.PORT,
|
||||
};
|
||||
const { host: koboldHost, port: koboldPort } = parseKoboldConfig(args);
|
||||
|
||||
sendKoboldOutput(`Starting ComfyUI frontend on port ${config.port}...`);
|
||||
|
||||
await ensureComfyUIInstalled();
|
||||
|
||||
const installDir = getInstallDir();
|
||||
const comfyUIWorkspace = join(installDir, 'comfyui-workspace');
|
||||
const pythonPath = getPythonPath(comfyUIWorkspace);
|
||||
|
||||
const comfyUIArgs = [
|
||||
join(comfyUIWorkspace, 'main.py'),
|
||||
'--port',
|
||||
config.port.toString(),
|
||||
'--enable-cors-header',
|
||||
];
|
||||
|
||||
const forceCPU = await shouldForceCPUMode();
|
||||
if (forceCPU) {
|
||||
comfyUIArgs.push('--cpu');
|
||||
sendKoboldOutput(
|
||||
'Forcing ComfyUI to use CPU mode (no compatible GPU acceleration available)'
|
||||
);
|
||||
}
|
||||
|
||||
if (koboldHost && koboldPort) {
|
||||
sendKoboldOutput(
|
||||
`Connecting to KoboldCpp at ${koboldHost}:${koboldPort}`
|
||||
);
|
||||
}
|
||||
|
||||
sendKoboldOutput(`Running ComfyUI with Python: ${pythonPath}`);
|
||||
sendKoboldOutput(`ComfyUI args: ${comfyUIArgs.join(' ')}`);
|
||||
|
||||
comfyUIProcess = spawn(pythonPath, comfyUIArgs, {
|
||||
cwd: comfyUIWorkspace,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
const version = await app.getVersion();
|
||||
sendKoboldOutput(`ComfyUI started via Gerbil v${version}`);
|
||||
|
||||
if (comfyUIProcess.stdout) {
|
||||
comfyUIProcess.stdout.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
if (comfyUIProcess.stderr) {
|
||||
comfyUIProcess.stderr.on('data', (data: Buffer) => {
|
||||
sendKoboldOutput(data.toString('utf8'), true);
|
||||
});
|
||||
}
|
||||
|
||||
comfyUIProcess.on('exit', (code: number | null, signal: string | null) => {
|
||||
if (code === null && signal) {
|
||||
sendKoboldOutput(`ComfyUI process terminated by signal: ${signal}`);
|
||||
} else {
|
||||
sendKoboldOutput(`ComfyUI process exited with code: ${code}`);
|
||||
}
|
||||
comfyUIProcess = null;
|
||||
});
|
||||
|
||||
comfyUIProcess.on('error', (error: Error) => {
|
||||
logError('ComfyUI process error', error);
|
||||
comfyUIProcess = null;
|
||||
});
|
||||
|
||||
await waitForComfyUIToStart();
|
||||
} catch (error) {
|
||||
logError('Failed to start ComfyUI', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const stopFrontend = () => terminateProcess(comfyUIProcess);
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { readFile, writeFile, copyFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { terminateProcess } from '@/utils/node/process';
|
||||
import { logError, tryExecute, safeExecute } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from '../window';
|
||||
import { logError, safeExecute } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from '@/main/modules/window';
|
||||
import { SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { KLITE_CSS_OVERRIDE } from '@/constants/patches';
|
||||
import { pathExists } from '@/utils/node/fs';
|
||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||
import { getAssetPath } from '@/utils/node/path';
|
||||
import { getCurrentVersion } from './version';
|
||||
import { getCurrentKoboldBinary, get as getConfig } from '../config';
|
||||
import { getCurrentVersion } from '../version';
|
||||
import {
|
||||
getCurrentKoboldBinary,
|
||||
get as getConfig,
|
||||
} from '@/main/modules/config';
|
||||
import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern';
|
||||
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||
import { startFrontend as startComfyUIFrontend } from '@/main/modules/comfyui';
|
||||
import { patchKliteEmbd, patchKcppSduiEmbd, filterSpam } from './patches';
|
||||
import { startProxy, stopProxy } from '../proxy';
|
||||
import type {
|
||||
FrontendPreference,
|
||||
ImageGenerationFrontendPreference,
|
||||
|
|
@ -22,70 +22,6 @@ import type {
|
|||
|
||||
let koboldProcess: ChildProcess | null = null;
|
||||
|
||||
const patchKliteEmbd = (unpackedDir: string) =>
|
||||
tryExecute(async () => {
|
||||
const possiblePaths = [
|
||||
join(unpackedDir, '_internal', 'embd_res', 'klite.embd'),
|
||||
join(unpackedDir, 'klite.embd'),
|
||||
];
|
||||
|
||||
let kliteEmbdPath: string | null = null;
|
||||
for (const path of possiblePaths) {
|
||||
if (await pathExists(path)) {
|
||||
kliteEmbdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!kliteEmbdPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await readFile(kliteEmbdPath, 'utf8');
|
||||
|
||||
if (content.includes('</head>')) {
|
||||
let patchedContent = content;
|
||||
|
||||
if (content.includes('gerbil-css-override')) {
|
||||
patchedContent = patchedContent.replace(
|
||||
/<style id="gerbil-css-override">[\s\S]*?<\/style>\s*/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
if (content.includes('gerbil-autoscroll-patches')) {
|
||||
patchedContent = patchedContent.replace(
|
||||
/<script id="gerbil-autoscroll-patches">[\s\S]*?<\/script>\s*/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
patchedContent = patchedContent.replace(
|
||||
'</head>',
|
||||
`${KLITE_CSS_OVERRIDE}\n</head>`
|
||||
);
|
||||
|
||||
await writeFile(kliteEmbdPath, patchedContent, 'utf8');
|
||||
}
|
||||
}, 'Failed to patch klite.embd');
|
||||
|
||||
const patchKcppSduiEmbd = (unpackedDir: string) =>
|
||||
tryExecute(async () => {
|
||||
const possiblePaths = [
|
||||
join(unpackedDir, '_internal', 'embd_res', 'kcpp_sdui.embd'),
|
||||
join(unpackedDir, 'kcpp_sdui.embd'),
|
||||
];
|
||||
|
||||
const sourceAssetPath = getAssetPath('kcpp_sdui.embd');
|
||||
|
||||
for (const targetPath of possiblePaths) {
|
||||
if (await pathExists(targetPath)) {
|
||||
await copyFile(sourceAssetPath, targetPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 'Failed to patch kcpp_sdui.embd');
|
||||
|
||||
export async function launchKoboldCpp(
|
||||
args: string[] = [],
|
||||
frontendPreference: FrontendPreference = 'koboldcpp',
|
||||
|
|
@ -129,6 +65,9 @@ export async function launchKoboldCpp(
|
|||
}
|
||||
|
||||
const finalArgs = [...args];
|
||||
const { host: koboldHost, port: koboldPort } = parseKoboldConfig(args);
|
||||
|
||||
await startProxy(koboldHost, koboldPort);
|
||||
|
||||
const child = spawn(currentVersion.path, finalArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
|
|
@ -144,7 +83,7 @@ export async function launchKoboldCpp(
|
|||
let readyResolve:
|
||||
| ((value: { success: boolean; pid?: number; error?: string }) => void)
|
||||
| null = null;
|
||||
let _readyReject: ((error: Error) => void) | null = null;
|
||||
let readyReject: ((error: Error) => void) | null = null;
|
||||
let isReady = false;
|
||||
|
||||
const readyPromise = new Promise<{
|
||||
|
|
@ -153,12 +92,15 @@ export async function launchKoboldCpp(
|
|||
error?: string;
|
||||
}>((resolve, reject) => {
|
||||
readyResolve = resolve;
|
||||
_readyReject = reject;
|
||||
readyReject = reject;
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
sendKoboldOutput(output, true);
|
||||
const filtered = filterSpam(output);
|
||||
if (filtered.trim()) {
|
||||
sendKoboldOutput(filtered, true);
|
||||
}
|
||||
|
||||
if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
|
||||
isReady = true;
|
||||
|
|
@ -168,7 +110,10 @@ export async function launchKoboldCpp(
|
|||
|
||||
child.stderr?.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
sendKoboldOutput(output, true);
|
||||
const filtered = filterSpam(output);
|
||||
if (filtered.trim()) {
|
||||
sendKoboldOutput(filtered, true);
|
||||
}
|
||||
|
||||
if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
|
||||
isReady = true;
|
||||
|
|
@ -188,7 +133,7 @@ export async function launchKoboldCpp(
|
|||
koboldProcess = null;
|
||||
|
||||
if (!isReady) {
|
||||
_readyReject?.(
|
||||
readyReject?.(
|
||||
new Error(
|
||||
`Process exited before ready signal (code: ${code}, signal: ${signal})`
|
||||
)
|
||||
|
|
@ -203,7 +148,7 @@ export async function launchKoboldCpp(
|
|||
koboldProcess = null;
|
||||
|
||||
if (!isReady) {
|
||||
_readyReject?.(error);
|
||||
readyReject?.(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -215,17 +160,23 @@ export async function launchKoboldCpp(
|
|||
}
|
||||
}
|
||||
|
||||
export const stopKoboldCpp = () => terminateProcess(koboldProcess);
|
||||
export async function stopKoboldCpp() {
|
||||
await stopProxy();
|
||||
return terminateProcess(koboldProcess);
|
||||
}
|
||||
|
||||
export const launchKoboldCppWithCustomFrontends = async (args: string[] = []) =>
|
||||
safeExecute(async () => {
|
||||
const frontendPreference = (await getConfig(
|
||||
'frontendPreference'
|
||||
)) as FrontendPreference;
|
||||
const [frontendPreference, imageGenerationFrontendPreference] =
|
||||
(await Promise.all([
|
||||
getConfig('frontendPreference'),
|
||||
getConfig('imageGenerationFrontendPreference'),
|
||||
])) as [
|
||||
FrontendPreference,
|
||||
ImageGenerationFrontendPreference | undefined,
|
||||
];
|
||||
|
||||
const imageGenerationFrontendPreference = (await getConfig(
|
||||
'imageGenerationFrontendPreference'
|
||||
)) as ImageGenerationFrontendPreference | undefined;
|
||||
const { isTextMode } = parseKoboldConfig(args);
|
||||
|
||||
const result = await launchKoboldCpp(
|
||||
args,
|
||||
|
|
@ -233,8 +184,6 @@ export const launchKoboldCppWithCustomFrontends = async (args: string[] = []) =>
|
|||
imageGenerationFrontendPreference
|
||||
);
|
||||
|
||||
const { isImageMode, isTextMode } = parseKoboldConfig(args);
|
||||
|
||||
if (
|
||||
frontendPreference === 'koboldcpp' ||
|
||||
(!isTextMode && imageGenerationFrontendPreference === 'builtin')
|
||||
|
|
@ -246,8 +195,6 @@ export const launchKoboldCppWithCustomFrontends = async (args: string[] = []) =>
|
|||
startSillyTavernFrontend(args);
|
||||
} else if (frontendPreference === 'openwebui') {
|
||||
startOpenWebUIFrontend(args);
|
||||
} else if (frontendPreference === 'comfyui' && isImageMode) {
|
||||
startComfyUIFrontend(args);
|
||||
}
|
||||
|
||||
return result;
|
||||
149
src/main/modules/koboldcpp/launcher/patches.ts
Normal file
149
src/main/modules/koboldcpp/launcher/patches.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { readFile, writeFile, copyFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { tryExecute } from '@/utils/node/logging';
|
||||
import { pathExists } from '@/utils/node/fs';
|
||||
import { getAssetPath } from '@/utils/node/path';
|
||||
|
||||
const KLITE_CSS_OVERRIDE = `
|
||||
<style id="gerbil-css-override">
|
||||
* {
|
||||
transition: 100ms ease all;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.adaptivecontainer {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#lastreq1 {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
#inputrow {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#navbarNavDropdown {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#actionmenuitems {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#inputrow > :nth-child(1) {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
#inputrow.show_mode > :nth-child(1) {
|
||||
flex: 0 0 70px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
#inputrow > :nth-child(3) {
|
||||
flex: 0 0 70px;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
#inputrow.show_mode > :nth-child(3) button {
|
||||
background-color: #129c00;
|
||||
font-size: 14px;
|
||||
}
|
||||
#inputrow.show_mode > :nth-child(3) button:hover {
|
||||
background-color: #058105;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
export const patchKliteEmbd = (unpackedDir: string) =>
|
||||
tryExecute(async () => {
|
||||
const possiblePaths = [
|
||||
join(unpackedDir, '_internal', 'embd_res', 'klite.embd'),
|
||||
join(unpackedDir, 'klite.embd'),
|
||||
];
|
||||
|
||||
let kliteEmbdPath: string | null = null;
|
||||
for (const path of possiblePaths) {
|
||||
if (await pathExists(path)) {
|
||||
kliteEmbdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!kliteEmbdPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await readFile(kliteEmbdPath, 'utf8');
|
||||
|
||||
if (content.includes('</head>')) {
|
||||
let patchedContent = content;
|
||||
|
||||
if (content.includes('gerbil-css-override')) {
|
||||
patchedContent = patchedContent.replace(
|
||||
/<style id="gerbil-css-override">[\s\S]*?<\/style>\s*/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
if (content.includes('gerbil-autoscroll-patches')) {
|
||||
patchedContent = patchedContent.replace(
|
||||
/<script id="gerbil-autoscroll-patches">[\s\S]*?<\/script>\s*/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
patchedContent = patchedContent.replace(
|
||||
'</head>',
|
||||
`${KLITE_CSS_OVERRIDE}\n</head>`
|
||||
);
|
||||
|
||||
await writeFile(kliteEmbdPath, patchedContent, 'utf8');
|
||||
}
|
||||
}, 'Failed to patch klite.embd');
|
||||
|
||||
export const patchKcppSduiEmbd = (unpackedDir: string) =>
|
||||
tryExecute(async () => {
|
||||
const possiblePaths = [
|
||||
join(unpackedDir, '_internal', 'embd_res', 'kcpp_sdui.embd'),
|
||||
join(unpackedDir, 'kcpp_sdui.embd'),
|
||||
];
|
||||
|
||||
const sourceAssetPath = getAssetPath('kcpp_sdui.embd');
|
||||
|
||||
for (const targetPath of possiblePaths) {
|
||||
if (await pathExists(targetPath)) {
|
||||
await copyFile(sourceAssetPath, targetPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 'Failed to patch kcpp_sdui.embd');
|
||||
|
||||
export function filterSpam(output: string) {
|
||||
const spamPatterns = [
|
||||
/^print_info:/,
|
||||
/^llama_model_load_from_file_impl:/,
|
||||
/^llama_model_loader:/,
|
||||
/^init_tokenizer:/,
|
||||
/^load:/,
|
||||
/^load_tensors:/,
|
||||
/^llama_context:/,
|
||||
/^llama_kv_cache/,
|
||||
/^set_abort_callback:/,
|
||||
/^attach_threadpool:/,
|
||||
/^ggml_vulkan:/,
|
||||
/^Namespace\(/,
|
||||
/^==========$/,
|
||||
/^Loading Chat Completions Adapter:/,
|
||||
/^Chat Completions Adapter Loaded$/,
|
||||
/^Chat completion heuristic:/,
|
||||
/^Embedded .* loaded\.$/,
|
||||
];
|
||||
|
||||
return output
|
||||
.split('\n')
|
||||
.filter((line) => !spamPatterns.some((pattern) => pattern.test(line)))
|
||||
.map((line) => line.replace(/^Welcome to KoboldCpp - Version/, 'Version'))
|
||||
.join('\n');
|
||||
}
|
||||
107
src/main/modules/koboldcpp/proxy.ts
Normal file
107
src/main/modules/koboldcpp/proxy.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import http from 'http';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
import { logError } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from '../window';
|
||||
import { PROXY } from '@/constants/proxy';
|
||||
|
||||
let proxyServer: http.Server | null = null;
|
||||
let koboldCppHost = 'localhost';
|
||||
let koboldCppPort = 5001;
|
||||
|
||||
const replaceKoboldWithGerbil = (data: string) => {
|
||||
try {
|
||||
return data.replace(/"koboldcpp\//g, '"gerbil/');
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const proxyRequest = (
|
||||
clientReq: IncomingMessage,
|
||||
clientRes: ServerResponse
|
||||
) => {
|
||||
const options = {
|
||||
hostname: koboldCppHost,
|
||||
port: koboldCppPort,
|
||||
path: clientReq.url,
|
||||
method: clientReq.method,
|
||||
headers: clientReq.headers,
|
||||
};
|
||||
|
||||
const proxyReq = http.request(options, (proxyRes) => {
|
||||
const isJson =
|
||||
proxyRes.headers['content-type']?.includes('application/json');
|
||||
|
||||
if (isJson) {
|
||||
let body = '';
|
||||
|
||||
proxyRes.on('data', (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
proxyRes.on('end', () => {
|
||||
const modifiedBody = replaceKoboldWithGerbil(body);
|
||||
|
||||
clientRes.writeHead(proxyRes.statusCode || 200, {
|
||||
...proxyRes.headers,
|
||||
'content-length': Buffer.byteLength(modifiedBody),
|
||||
});
|
||||
clientRes.end(modifiedBody);
|
||||
});
|
||||
} else {
|
||||
clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
||||
proxyRes.pipe(clientRes);
|
||||
}
|
||||
});
|
||||
|
||||
proxyReq.on('error', (error) => {
|
||||
logError(`Proxy request error: ${error.message}`, error);
|
||||
if (!clientRes.headersSent) {
|
||||
clientRes.writeHead(502);
|
||||
clientRes.end('Bad Gateway');
|
||||
}
|
||||
});
|
||||
|
||||
clientReq.pipe(proxyReq);
|
||||
};
|
||||
|
||||
export const startProxy = (targetHost: string, targetPort: number) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
if (proxyServer) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
koboldCppHost = targetHost;
|
||||
koboldCppPort = targetPort;
|
||||
|
||||
proxyServer = http.createServer((req, res) => {
|
||||
proxyRequest(req, res);
|
||||
});
|
||||
|
||||
proxyServer.on('error', (error) => {
|
||||
logError(`Proxy server error: ${error.message}`, error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
proxyServer.listen(PROXY.PORT, PROXY.HOST, () => {
|
||||
sendKoboldOutput(`Proxy server started on port ${PROXY.PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
export const stopProxy = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
if (!proxyServer) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
proxyServer.close(() => {
|
||||
proxyServer = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
export const isProxyRunning = () => proxyServer !== null;
|
||||
|
|
@ -11,6 +11,7 @@ import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
|||
import { terminateProcess } from '@/utils/node/process';
|
||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||
import { getUvEnvironment } from './dependencies';
|
||||
import { PROXY } from '@/constants/proxy';
|
||||
|
||||
let openWebUIProcess: ChildProcess | null = null;
|
||||
|
||||
|
|
@ -79,6 +80,7 @@ export async function startFrontend(args: string[]) {
|
|||
);
|
||||
|
||||
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||
const proxyUrl = PROXY.URL;
|
||||
|
||||
const openWebUIArgs = [
|
||||
...OPENWEBUI_BASE_ARGS,
|
||||
|
|
@ -94,7 +96,7 @@ export async function startFrontend(args: string[]) {
|
|||
const openWebUIDataDir = join(installDir, 'openwebui-data');
|
||||
|
||||
const envConfig: Record<string, string> = {
|
||||
OPENAI_API_BASE_URL: `${koboldUrl}/v1`,
|
||||
OPENAI_API_BASE_URL: `${proxyUrl}/v1`,
|
||||
DATA_DIR: openWebUIDataDir,
|
||||
WEBUI_AUTH: 'false',
|
||||
WEBUI_SECRET_KEY: 'gerbil',
|
||||
|
|
@ -150,7 +152,7 @@ export async function startFrontend(args: string[]) {
|
|||
|
||||
sendKoboldOutput(`Open WebUI is ready and auto-configured!`);
|
||||
sendKoboldOutput(`Access Open WebUI at: http://localhost:${config.port}`);
|
||||
sendKoboldOutput(`Text Generation: ${koboldUrl}/v1 (auto-configured)`);
|
||||
sendKoboldOutput(`Text Generation: ${proxyUrl}/v1 (auto-configured)`);
|
||||
if (isImageMode) {
|
||||
sendKoboldOutput(`Image Generation: ${koboldUrl} (auto-configured)`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { ChildProcess } from 'child_process';
|
|||
import { logError, tryExecute } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from './window';
|
||||
import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { PROXY } from '@/constants/proxy';
|
||||
import { terminateProcess } from '@/utils/node/process';
|
||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||
|
|
@ -97,26 +98,17 @@ async function ensureSillyTavernSettings() {
|
|||
let hasResolved = false;
|
||||
|
||||
initProcess.on('exit', (code: number | null, signal: string | null) => {
|
||||
sendKoboldOutput(
|
||||
signal
|
||||
? `SillyTavern init process terminated with signal ${signal}`
|
||||
: `SillyTavern init process exited with code ${code}`
|
||||
);
|
||||
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
|
||||
if (code !== 0) {
|
||||
const errorMsg =
|
||||
code === 4294963214
|
||||
? 'SillyTavern failed to install due to EBUSY error (resource busy or locked). This is a critical error.'
|
||||
: `SillyTavern initialization failed with exit code ${code}`;
|
||||
const errorMsg = signal
|
||||
? `SillyTavern init terminated with signal ${signal}`
|
||||
: `SillyTavern initialization failed with exit code ${code}`;
|
||||
|
||||
logError('SillyTavern initialization failed:', new Error(errorMsg));
|
||||
sendKoboldOutput(`CRITICAL ERROR: ${errorMsg}`);
|
||||
logError(errorMsg, new Error(errorMsg));
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
sendKoboldOutput('SillyTavern settings should now be generated');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
|
@ -125,8 +117,7 @@ async function ensureSillyTavernSettings() {
|
|||
initProcess.on('error', (error) => {
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
logError('Failed to initialize SillyTavern settings:', error);
|
||||
sendKoboldOutput(`SillyTavern initialization error: ${error.message}`);
|
||||
logError('SillyTavern initialization error', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
|
@ -159,11 +150,7 @@ async function ensureSillyTavernSettings() {
|
|||
});
|
||||
}
|
||||
|
||||
async function setupSillyTavernConfig(
|
||||
koboldHost: string,
|
||||
koboldPort: number,
|
||||
isImageMode: boolean
|
||||
) {
|
||||
async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||
const success = await tryExecute(async () => {
|
||||
const configPath = getSillyTavernSettingsPath();
|
||||
let settings: Record<string, unknown> = {};
|
||||
|
|
@ -181,23 +168,12 @@ async function setupSillyTavernConfig(
|
|||
}
|
||||
}
|
||||
|
||||
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||
const proxyUrl = PROXY.URL;
|
||||
|
||||
if (!settings.power_user) settings.power_user = {};
|
||||
const powerUser = settings.power_user as Record<string, unknown>;
|
||||
powerUser.auto_connect = true;
|
||||
|
||||
if (isImageMode) {
|
||||
sendKoboldOutput(
|
||||
`Image generation mode detected. Please configure SillyTavern manually:\n` +
|
||||
`1. Open SillyTavern and navigate to Settings (top-right gear icon)\n` +
|
||||
`2. Go to 'Extensions' tab and enable 'Image Generation'\n` +
|
||||
`3. In Image Generation settings, set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` +
|
||||
`4. Set API URL to: ${koboldUrl}\n` +
|
||||
`5. Click 'Connect' to test the connection`
|
||||
);
|
||||
}
|
||||
|
||||
if (!settings.textgenerationwebui_settings)
|
||||
settings.textgenerationwebui_settings = {};
|
||||
const textgenSettings = settings.textgenerationwebui_settings as Record<
|
||||
|
|
@ -207,15 +183,26 @@ async function setupSillyTavernConfig(
|
|||
|
||||
if (!textgenSettings.server_urls) textgenSettings.server_urls = {};
|
||||
const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
|
||||
serverUrls.koboldcpp = koboldUrl;
|
||||
serverUrls.koboldcpp = proxyUrl;
|
||||
|
||||
settings.main_api = 'textgenerationwebui';
|
||||
textgenSettings.type = 'koboldcpp';
|
||||
|
||||
sendKoboldOutput(
|
||||
`Configured SillyTavern for text generation at ${koboldUrl}`
|
||||
`Configured SillyTavern for text generation at ${proxyUrl}`
|
||||
);
|
||||
|
||||
if (isImageMode) {
|
||||
sendKoboldOutput(
|
||||
`Image generation mode detected. Configure SillyTavern manually:\n` +
|
||||
`1. Open SillyTavern Settings (top-right gear icon)\n` +
|
||||
`2. Go to 'Extensions' tab and enable 'Image Generation'\n` +
|
||||
`3. Set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` +
|
||||
`4. Set API URL to: ${proxyUrl}/sdui\n` +
|
||||
`5. Click 'Connect' to test the connection`
|
||||
);
|
||||
}
|
||||
|
||||
await writeJsonFile(configPath, settings);
|
||||
|
||||
sendKoboldOutput(`SillyTavern configuration updated successfully!`);
|
||||
|
|
@ -251,7 +238,7 @@ async function waitForSillyTavernToStart(_port: number) {
|
|||
});
|
||||
}
|
||||
|
||||
function createProxyServer(targetPort: number, proxyPort: number) {
|
||||
const createProxyServer = (targetPort: number, proxyPort: number) => {
|
||||
proxyServer = createServer((req, res) => {
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
|
|
@ -278,15 +265,13 @@ function createProxyServer(targetPort: number, proxyPort: number) {
|
|||
});
|
||||
|
||||
proxyServer.listen(proxyPort, () => {
|
||||
sendKoboldOutput(
|
||||
`Proxy server started on port ${proxyPort}, forwarding to SillyTavern on port ${targetPort}`
|
||||
);
|
||||
sendKoboldOutput(`SillyTavern CORS proxy started on port ${proxyPort}`);
|
||||
});
|
||||
|
||||
proxyServer.on('error', (err) => {
|
||||
logError('Proxy server error:', err);
|
||||
logError('SillyTavern proxy error:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export async function startFrontend(args: string[]) {
|
||||
try {
|
||||
|
|
@ -295,20 +280,14 @@ export async function startFrontend(args: string[]) {
|
|||
port: SILLYTAVERN.PORT,
|
||||
proxyPort: SILLYTAVERN.PROXY_PORT,
|
||||
};
|
||||
const {
|
||||
host: koboldHost,
|
||||
port: koboldPort,
|
||||
isImageMode,
|
||||
} = parseKoboldConfig(args);
|
||||
const { isImageMode } = parseKoboldConfig(args);
|
||||
|
||||
await stopFrontend();
|
||||
|
||||
sendKoboldOutput(
|
||||
`Preparing SillyTavern to connect at ${koboldHost}:${koboldPort}...`
|
||||
);
|
||||
sendKoboldOutput(`Preparing SillyTavern to connect via proxy...`);
|
||||
|
||||
await ensureSillyTavernSettings();
|
||||
await setupSillyTavernConfig(koboldHost, koboldPort, isImageMode);
|
||||
await setupSillyTavernConfig(isImageMode);
|
||||
|
||||
sendKoboldOutput(
|
||||
`Starting ${config.name} frontend on port ${config.port}...`
|
||||
|
|
@ -320,8 +299,6 @@ export async function startFrontend(args: string[]) {
|
|||
config.port.toString(),
|
||||
];
|
||||
|
||||
sendKoboldOutput('Final port check before starting SillyTavern...');
|
||||
|
||||
sillyTavernProcess = await createNpxProcess(sillyTavernArgs);
|
||||
|
||||
if (sillyTavernProcess.stdout) {
|
||||
|
|
|
|||
6
src/types/index.d.ts
vendored
6
src/types/index.d.ts
vendored
|
|
@ -12,11 +12,7 @@ export type ChatMode = 'text' | 'image';
|
|||
|
||||
export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
|
||||
|
||||
export type FrontendPreference =
|
||||
| 'koboldcpp'
|
||||
| 'sillytavern'
|
||||
| 'openwebui'
|
||||
| 'comfyui';
|
||||
export type FrontendPreference = 'koboldcpp' | 'sillytavern' | 'openwebui';
|
||||
|
||||
export type ImageGenerationFrontendPreference = 'match' | 'builtin';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import type {
|
|||
InterfaceTab,
|
||||
ImageGenerationFrontendPreference,
|
||||
} from '@/types';
|
||||
import { FRONTENDS, SILLYTAVERN, OPENWEBUI, COMFYUI } from '@/constants';
|
||||
import { FRONTENDS, SILLYTAVERN, OPENWEBUI } from '@/constants';
|
||||
import { PROXY } from '@/constants/proxy';
|
||||
|
||||
export interface InterfaceOption {
|
||||
value: InterfaceTab | 'eject';
|
||||
|
|
@ -58,12 +59,7 @@ export function getAvailableInterfaceOptions({
|
|||
}
|
||||
|
||||
if (isImageGenerationMode) {
|
||||
if (effectiveImageFrontend === 'comfyui') {
|
||||
chatItems.push({
|
||||
value: 'chat-image',
|
||||
label: FRONTENDS.COMFYUI,
|
||||
});
|
||||
} else if (effectiveImageFrontend === 'koboldcpp') {
|
||||
if (effectiveImageFrontend === 'koboldcpp') {
|
||||
chatItems.push({
|
||||
value: 'chat-image',
|
||||
label: FRONTENDS.STABLE_UI,
|
||||
|
|
@ -102,10 +98,6 @@ export function getDefaultInterfaceTab({
|
|||
return 'chat-text';
|
||||
}
|
||||
|
||||
if (effectiveImageFrontend === 'comfyui' && isImageGenerationMode) {
|
||||
return 'chat-image';
|
||||
}
|
||||
|
||||
if (isTextMode) {
|
||||
return 'chat-text';
|
||||
}
|
||||
|
|
@ -122,23 +114,25 @@ export interface ServerInterfaceInfo {
|
|||
title: string;
|
||||
}
|
||||
|
||||
export interface ServerInterfaceParams {
|
||||
frontendPreference: FrontendPreference;
|
||||
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference;
|
||||
isImageGenerationMode: boolean;
|
||||
}
|
||||
|
||||
export function getServerInterfaceInfo({
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference = 'match',
|
||||
isImageGenerationMode,
|
||||
serverUrl,
|
||||
}: {
|
||||
frontendPreference: FrontendPreference;
|
||||
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference;
|
||||
isImageGenerationMode: boolean;
|
||||
serverUrl: string;
|
||||
}) {
|
||||
}: ServerInterfaceParams) {
|
||||
const proxyUrl = PROXY.URL;
|
||||
|
||||
if (
|
||||
isImageGenerationMode &&
|
||||
imageGenerationFrontendPreference === 'builtin'
|
||||
) {
|
||||
return {
|
||||
url: `${serverUrl}/sdui`,
|
||||
url: `${proxyUrl}/sdui`,
|
||||
title: FRONTENDS.STABLE_UI,
|
||||
};
|
||||
}
|
||||
|
|
@ -157,15 +151,8 @@ export function getServerInterfaceInfo({
|
|||
};
|
||||
}
|
||||
|
||||
if (frontendPreference === 'comfyui' && isImageGenerationMode) {
|
||||
return {
|
||||
url: COMFYUI.URL,
|
||||
title: FRONTENDS.COMFYUI,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: isImageGenerationMode ? `${serverUrl}/sdui` : serverUrl,
|
||||
url: isImageGenerationMode ? `${proxyUrl}/sdui` : proxyUrl,
|
||||
title: isImageGenerationMode
|
||||
? FRONTENDS.STABLE_UI
|
||||
: FRONTENDS.KOBOLDAI_LITE,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue