mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
Compare commits
4 commits
58eba745a2
...
8e39cba942
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e39cba942 | |||
| ffb1297b03 | |||
|
|
0aa967be9e | ||
|
|
350c607d7c |
12 changed files with 203 additions and 122 deletions
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
[](https://github.com/lone-cloud/gerbil/releases)
|
[](https://github.com/lone-cloud/gerbil/releases)
|
||||||
[](https://github.com/lone-cloud/gerbil/stargazers)
|
|
||||||
[](https://aur.archlinux.org/packages/gerbil)
|
[](https://aur.archlinux.org/packages/gerbil)
|
||||||
|
|
||||||
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;">
|
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;">
|
||||||
|
|
|
||||||
BIN
assets/badges/github-badge.png
Normal file
BIN
assets/badges/github-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -1,11 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"version": "1.24.1",
|
"version": "1.24.2",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai",
|
"ai",
|
||||||
|
"desktop",
|
||||||
"electron",
|
"electron",
|
||||||
"koboldcpp",
|
"gguf",
|
||||||
|
"image-generation",
|
||||||
"language-model",
|
"language-model",
|
||||||
"llm",
|
"llm",
|
||||||
"local-ai",
|
"local-ai",
|
||||||
|
|
@ -58,7 +60,7 @@
|
||||||
"electron": "^41.5.0",
|
"electron": "^41.5.0",
|
||||||
"electron-builder": "^26.8.1",
|
"electron-builder": "^26.8.1",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.7.0",
|
||||||
"lucide-react": "^1.14.0",
|
"lucide-react": "^1.14.0",
|
||||||
"oxfmt": "^0.48.0",
|
"oxfmt": "^0.48.0",
|
||||||
"oxlint": "^1.63.0",
|
"oxlint": "^1.63.0",
|
||||||
|
|
|
||||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
|
|
@ -77,7 +77,7 @@ importers:
|
||||||
version: 4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.41.1)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
version: 4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.41.1)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))
|
version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0))
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: ^10.1.0
|
specifier: ^10.1.0
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
|
|
@ -89,10 +89,10 @@ importers:
|
||||||
version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
|
version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
|
||||||
electron-vite:
|
electron-vite:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))
|
version: 5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0))
|
||||||
jiti:
|
jiti:
|
||||||
specifier: ^2.6.1
|
specifier: ^2.7.0
|
||||||
version: 2.6.1
|
version: 2.7.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.14.0(react@19.2.5)
|
version: 1.14.0(react@19.2.5)
|
||||||
|
|
@ -137,7 +137,7 @@ importers:
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.10
|
specifier: ^8.0.10
|
||||||
version: 8.0.10(@types/node@25.6.0)(jiti@2.6.1)
|
version: 8.0.10(@types/node@25.6.0)(jiti@2.7.0)
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.13
|
specifier: ^5.0.13
|
||||||
version: 5.0.13(@types/react@19.2.14)(react@19.2.5)
|
version: 5.0.13(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
|
@ -1449,8 +1449,8 @@ packages:
|
||||||
electron-publish@26.8.1:
|
electron-publish@26.8.1:
|
||||||
resolution: {integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==}
|
resolution: {integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.349:
|
electron-to-chromium@1.5.350:
|
||||||
resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==}
|
resolution: {integrity: sha512-/KWD4qK8nMqIoJh35Rpc37fiVyOe80mcUQKpfje0Dp9uot2ROuipsh+EriCdfInxjleD5v1S4OlIn41I0LXP0g==}
|
||||||
|
|
||||||
electron-updater@6.8.3:
|
electron-updater@6.8.3:
|
||||||
resolution: {integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==}
|
resolution: {integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==}
|
||||||
|
|
@ -1848,8 +1848,8 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
jiti@2.6.1:
|
jiti@2.7.0:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
|
|
@ -3770,10 +3770,10 @@ snapshots:
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))':
|
'@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||||
vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1)
|
vite: 8.0.10(@types/node@25.6.0)(jiti@2.7.0)
|
||||||
|
|
||||||
'@xmldom/xmldom@0.8.13': {}
|
'@xmldom/xmldom@0.8.13': {}
|
||||||
|
|
||||||
|
|
@ -3834,7 +3834,7 @@ snapshots:
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
hosted-git-info: 4.1.0
|
hosted-git-info: 4.1.0
|
||||||
isbinaryfile: 5.0.7
|
isbinaryfile: 5.0.7
|
||||||
jiti: 2.6.1
|
jiti: 2.7.0
|
||||||
js-yaml: 4.1.1
|
js-yaml: 4.1.1
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
|
|
@ -3896,7 +3896,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.27
|
baseline-browser-mapping: 2.10.27
|
||||||
caniuse-lite: 1.0.30001791
|
caniuse-lite: 1.0.30001791
|
||||||
electron-to-chromium: 1.5.349
|
electron-to-chromium: 1.5.350
|
||||||
node-releases: 2.0.38
|
node-releases: 2.0.38
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||||
|
|
||||||
|
|
@ -4220,7 +4220,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
electron-to-chromium@1.5.349: {}
|
electron-to-chromium@1.5.350: {}
|
||||||
|
|
||||||
electron-updater@6.8.3:
|
electron-updater@6.8.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -4235,7 +4235,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
electron-vite@5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)):
|
electron-vite@5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
|
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
|
||||||
|
|
@ -4243,7 +4243,7 @@ snapshots:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1)
|
vite: 8.0.10(@types/node@25.6.0)(jiti@2.7.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
@ -4721,7 +4721,7 @@ snapshots:
|
||||||
filelist: 1.0.6
|
filelist: 1.0.6
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
|
|
@ -5921,7 +5921,7 @@ snapshots:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1):
|
vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
@ -5931,7 +5931,7 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.6.0
|
'@types/node': 25.6.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.7.0
|
||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,13 @@ export const SystemTab = () => {
|
||||||
|
|
||||||
setVersionInfo(info);
|
setVersionInfo(info);
|
||||||
setKoboldVersion(currentBackend?.version ?? null);
|
setKoboldVersion(currentBackend?.version ?? null);
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError('Failed to load system info', error as Error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
|
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
|
||||||
window.electronAPI.kobold.detectCPU(),
|
window.electronAPI.kobold.detectCPU(),
|
||||||
window.electronAPI.kobold.detectGPU(),
|
window.electronAPI.kobold.detectGPU(),
|
||||||
|
|
@ -41,9 +47,7 @@ export const SystemTab = () => {
|
||||||
systemMemory,
|
systemMemory,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electronAPI.logs.logError('Failed to load system info', error as Error);
|
window.electronAPI.logs.logError('Failed to load hardware info', error as Error);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { cpus as osCpus, totalmem } from 'node:os';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation';
|
import { memLayout as siMemLayout } from 'systeminformation';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BasicGPUInfo,
|
BasicGPUInfo,
|
||||||
|
|
@ -33,36 +34,26 @@ let basicGPUInfoCache: BasicGPUInfo | null = null;
|
||||||
let gpuCapabilitiesCache: GPUCapabilities | null = null;
|
let gpuCapabilitiesCache: GPUCapabilities | null = null;
|
||||||
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
|
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
|
||||||
|
|
||||||
export async function detectCPU() {
|
export function detectCPU() {
|
||||||
if (cpuCapabilitiesCache) {
|
if (cpuCapabilitiesCache) {
|
||||||
return cpuCapabilitiesCache;
|
return cpuCapabilitiesCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await safeExecute(async () => {
|
const cpuList = osCpus();
|
||||||
const cpu = await siCpu();
|
const brand = cpuList[0]?.model;
|
||||||
|
const cores = cpuList.length;
|
||||||
|
const speed = (cpuList[0]?.speed ?? 0) / 1000;
|
||||||
|
|
||||||
const devices: { name: string; detailedName: string }[] = [];
|
const devices: { name: string; detailedName: string }[] = [];
|
||||||
if (cpu.brand) {
|
if (brand) {
|
||||||
const name = formatDeviceName(cpu.brand);
|
const name = formatDeviceName(brand);
|
||||||
|
|
||||||
devices.push({
|
devices.push({
|
||||||
detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`,
|
detailedName: `${name} (${cores} cores) @ ${speed} GHz`,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const capabilities = {
|
cpuCapabilitiesCache = { devices };
|
||||||
devices,
|
|
||||||
};
|
|
||||||
|
|
||||||
cpuCapabilitiesCache = capabilities;
|
|
||||||
return capabilities;
|
|
||||||
}, 'CPU detection failed');
|
|
||||||
|
|
||||||
cpuCapabilitiesCache = result ?? {
|
|
||||||
devices: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return cpuCapabilitiesCache;
|
return cpuCapabilitiesCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,10 +361,13 @@ export async function detectGPUMemory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const detectSystemMemory = async () => {
|
export const detectSystemMemory = async () => {
|
||||||
try {
|
const totalGB = (totalmem() / 1024 ** 3).toFixed(2);
|
||||||
const [memInfo, memLayout] = await Promise.all([siMem(), siMemLayout()]);
|
|
||||||
|
|
||||||
const totalGB = (memInfo.total / 1024 ** 3).toFixed(2);
|
try {
|
||||||
|
const memLayout = await Promise.race([
|
||||||
|
siMemLayout(),
|
||||||
|
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
||||||
|
]);
|
||||||
|
|
||||||
let speed: number | undefined;
|
let speed: number | undefined;
|
||||||
let type: string | undefined;
|
let type: string | undefined;
|
||||||
|
|
@ -394,15 +388,8 @@ export const detectSystemMemory = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { speed, totalGB, type };
|
||||||
speed,
|
|
||||||
totalGB,
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
const mem = await siMem();
|
return { totalGB };
|
||||||
return {
|
|
||||||
totalGB: (mem.total / 1024 ** 3).toFixed(2),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,7 @@ export async function launchKoboldCpp(
|
||||||
const resolvedArgs = await resolveModelPaths(args);
|
const resolvedArgs = await resolveModelPaths(args);
|
||||||
const finalArgs = resolvedArgs.filter((arg) => arg !== '--remotetunnel');
|
const finalArgs = resolvedArgs.filter((arg) => arg !== '--remotetunnel');
|
||||||
|
|
||||||
|
await stopProxy();
|
||||||
await startProxy(koboldHost, koboldPort);
|
await startProxy(koboldHost, koboldPort);
|
||||||
|
|
||||||
const rocmEnv =
|
const rocmEnv =
|
||||||
|
|
@ -251,7 +252,7 @@ export async function launchKoboldCpp(
|
||||||
|
|
||||||
readyResolve?.({ pid: child.pid, success: true });
|
readyResolve?.({ pid: child.pid, success: true });
|
||||||
|
|
||||||
if (remotetunnel) {
|
if (remotetunnel && isKoboldFrontend) {
|
||||||
void startTunnel(frontendPreference, true);
|
void startTunnel(frontendPreference, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -350,7 +351,7 @@ export const launchKoboldCppWithCustomFrontends = async (
|
||||||
const frontendPreference = getConfig('frontendPreference');
|
const frontendPreference = getConfig('frontendPreference');
|
||||||
const imageGenerationFrontendPreference = getConfig('imageGenerationFrontendPreference');
|
const imageGenerationFrontendPreference = getConfig('imageGenerationFrontendPreference');
|
||||||
|
|
||||||
const { isTextMode } = parseKoboldConfig(args);
|
const { isTextMode, remotetunnel } = parseKoboldConfig(args);
|
||||||
|
|
||||||
const result = await launchKoboldCpp(
|
const result = await launchKoboldCpp(
|
||||||
args,
|
args,
|
||||||
|
|
@ -367,17 +368,29 @@ export const launchKoboldCppWithCustomFrontends = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frontendPreference === 'sillytavern') {
|
if (frontendPreference === 'sillytavern') {
|
||||||
startSillyTavernFrontend(args).catch((error) => {
|
startSillyTavernFrontend(args)
|
||||||
|
.then(() => {
|
||||||
|
if (remotetunnel) {
|
||||||
|
void startTunnel(frontendPreference, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
logError('Failed to start SillyTavern frontend:', error);
|
logError('Failed to start SillyTavern frontend:', error);
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (frontendPreference === 'openwebui') {
|
} else if (frontendPreference === 'openwebui') {
|
||||||
startOpenWebUIFrontend(args).catch((error) => {
|
startOpenWebUIFrontend(args)
|
||||||
logError('Failed to start OpenWebUI frontend:', error);
|
.then(() => {
|
||||||
|
if (remotetunnel) {
|
||||||
|
void startTunnel(frontendPreference, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logError('Failed to start Open WebUI frontend:', error);
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Failed to start OpenWebUI: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to start Open WebUI: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export const patchKliteEmbd = (unpackedDir: string) =>
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
join(unpackedDir, '_internal', 'embd_res', 'klite.embd'),
|
join(unpackedDir, '_internal', 'embd_res', 'klite.embd'),
|
||||||
join(unpackedDir, 'embd_res', 'klite.embd'),
|
join(unpackedDir, 'embd_res', 'klite.embd'),
|
||||||
|
join(unpackedDir, 'klite.embd'),
|
||||||
];
|
];
|
||||||
|
|
||||||
let kliteEmbdPath: string | null = null;
|
let kliteEmbdPath: string | null = null;
|
||||||
|
|
@ -96,6 +97,7 @@ export const patchKcppSduiEmbd = (unpackedDir: string) =>
|
||||||
tryExecute(async () => {
|
tryExecute(async () => {
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
join(unpackedDir, '_internal', 'embd_res', 'kcpp_sdui.embd'),
|
join(unpackedDir, '_internal', 'embd_res', 'kcpp_sdui.embd'),
|
||||||
|
join(unpackedDir, 'embd_res', 'kcpp_sdui.embd'),
|
||||||
join(unpackedDir, 'kcpp_sdui.embd'),
|
join(unpackedDir, 'kcpp_sdui.embd'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -113,6 +115,7 @@ export const patchLcppGzEmbd = (unpackedDir: string) =>
|
||||||
tryExecute(async () => {
|
tryExecute(async () => {
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
join(unpackedDir, '_internal', 'embd_res', 'lcpp.gz.embd'),
|
join(unpackedDir, '_internal', 'embd_res', 'lcpp.gz.embd'),
|
||||||
|
join(unpackedDir, 'embd_res', 'lcpp.gz.embd'),
|
||||||
join(unpackedDir, 'lcpp.gz.embd'),
|
join(unpackedDir, 'lcpp.gz.embd'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ let memoryInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let gpuInterval: ReturnType<typeof setInterval> | null = null;
|
let gpuInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
const updateFrequency = 1000;
|
const updateFrequency = 1000;
|
||||||
|
const memoryUpdateFrequency = platform === 'win32' ? 3000 : updateFrequency;
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
let latestCpuMetrics: CpuMetrics | null = null;
|
let latestCpuMetrics: CpuMetrics | null = null;
|
||||||
|
|
@ -87,7 +88,7 @@ export function startMonitoring(window: BrowserWindow) {
|
||||||
void collectAndSendMemoryMetrics();
|
void collectAndSendMemoryMetrics();
|
||||||
memoryInterval = setInterval(() => {
|
memoryInterval = setInterval(() => {
|
||||||
void collectAndSendMemoryMetrics();
|
void collectAndSendMemoryMetrics();
|
||||||
}, updateFrequency);
|
}, memoryUpdateFrequency);
|
||||||
|
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
void collectAndSendGpuMetrics();
|
void collectAndSendGpuMetrics();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { ChildProcess } from 'node:child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import { readFile, unlink, writeFile } from 'node:fs/promises';
|
||||||
import { createServer, request } from 'node:http';
|
import { createServer, request } from 'node:http';
|
||||||
import type { Server } from 'node:http';
|
import type { Server } from 'node:http';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
@ -10,7 +11,7 @@ import { PROXY } from '@/constants/proxy';
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { logError, tryExecute } from '@/utils/node/logging';
|
import { logError, tryExecute } from '@/utils/node/logging';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { killProcessByPid, terminateProcess } from '@/utils/node/process';
|
||||||
|
|
||||||
import { getInstallDir } from './config';
|
import { getInstallDir } from './config';
|
||||||
import { getNodeEnvironment } from './dependencies';
|
import { getNodeEnvironment } from './dependencies';
|
||||||
|
|
@ -39,6 +40,27 @@ const getSillyTavernServerPath = () =>
|
||||||
const getSillyTavernSettingsPath = () =>
|
const getSillyTavernSettingsPath = () =>
|
||||||
join(getSillyTavernDataDir(), 'default-user', 'settings.json');
|
join(getSillyTavernDataDir(), 'default-user', 'settings.json');
|
||||||
|
|
||||||
|
const getSillyTavernPidPath = () => join(getSillyTavernInstallDir(), 'sillytavern.pid');
|
||||||
|
|
||||||
|
async function saveSillyTavernPid(pid: number) {
|
||||||
|
await writeFile(getSillyTavernPidPath(), String(pid), 'utf8').catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearSillyTavernPid() {
|
||||||
|
await unlink(getSillyTavernPidPath()).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killOrphanedSillyTavern() {
|
||||||
|
const pidPath = getSillyTavernPidPath();
|
||||||
|
if (!(await pathExists(pidPath))) return;
|
||||||
|
const raw = await readFile(pidPath, 'utf8').catch(() => null);
|
||||||
|
const pid = raw ? parseInt(raw.trim(), 10) : NaN;
|
||||||
|
if (!isNaN(pid)) {
|
||||||
|
await killProcessByPid(pid);
|
||||||
|
}
|
||||||
|
await clearSillyTavernPid();
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureSillyTavernInstalled() {
|
async function ensureSillyTavernInstalled() {
|
||||||
const serverPath = getSillyTavernServerPath();
|
const serverPath = getSillyTavernServerPath();
|
||||||
const installDir = getSillyTavernInstallDir();
|
const installDir = getSillyTavernInstallDir();
|
||||||
|
|
@ -73,7 +95,10 @@ async function ensureSillyTavernInstalled() {
|
||||||
sendKoboldOutput('Installing SillyTavern via npm...');
|
sendKoboldOutput('Installing SillyTavern via npm...');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
await killOrphanedSillyTavern();
|
||||||
|
|
||||||
|
const runNpmInstall = () =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
const npmProcess = spawn(
|
const npmProcess = spawn(
|
||||||
'npm',
|
'npm',
|
||||||
[
|
[
|
||||||
|
|
@ -105,17 +130,31 @@ async function ensureSillyTavernInstalled() {
|
||||||
sendKoboldOutput('SillyTavern is ready');
|
sendKoboldOutput('SillyTavern is ready');
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
if (errorOutput) {
|
reject(Object.assign(new Error(`npm install failed with code ${code}`), { errorOutput }));
|
||||||
sendKoboldOutput(`npm install error: ${errorOutput.trim()}`);
|
|
||||||
}
|
|
||||||
reject(new Error(`npm install failed with code ${code}`));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
npmProcess.on('error', (error) => {
|
npmProcess.on('error', reject);
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runNpmInstall();
|
||||||
|
} catch (err) {
|
||||||
|
const isBusy =
|
||||||
|
err instanceof Error && (err.message.includes('4294963214') || err.message.includes('-4082'));
|
||||||
|
|
||||||
|
if (isBusy && platform === 'win32') {
|
||||||
|
sendKoboldOutput('File busy, retrying npm install...');
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
|
||||||
|
await runNpmInstall();
|
||||||
|
} else {
|
||||||
|
const output = (err as { errorOutput?: string }).errorOutput;
|
||||||
|
if (output) {
|
||||||
|
sendKoboldOutput(`npm install error: ${output.trim()}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNpxProcess(args: string[]) {
|
async function createNpxProcess(args: string[]) {
|
||||||
|
|
@ -289,8 +328,15 @@ async function waitForSillyTavernToStart() {
|
||||||
|
|
||||||
const createProxyServer = (targetPort: number, proxyPort: number) => {
|
const createProxyServer = (targetPort: number, proxyPort: number) => {
|
||||||
proxyServer = createServer((req, res) => {
|
proxyServer = createServer((req, res) => {
|
||||||
|
const headers = { ...req.headers };
|
||||||
|
delete headers['x-forwarded-for'];
|
||||||
|
delete headers['x-real-ip'];
|
||||||
|
delete headers['cf-connecting-ip'];
|
||||||
|
delete headers['x-forwarded-host'];
|
||||||
|
delete headers.forwarded;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: req.headers,
|
headers,
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.url,
|
path: req.url,
|
||||||
|
|
@ -298,9 +344,9 @@ const createProxyServer = (targetPort: number, proxyPort: number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxyReq = request(options, (proxyRes) => {
|
const proxyReq = request(options, (proxyRes) => {
|
||||||
const headers = { ...proxyRes.headers };
|
const responseHeaders = { ...proxyRes.headers };
|
||||||
delete headers['x-frame-options'];
|
delete responseHeaders['x-frame-options'];
|
||||||
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);
|
||||||
proxyRes.pipe(res);
|
proxyRes.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -341,6 +387,8 @@ export async function startFrontend(args: string[]) {
|
||||||
|
|
||||||
sendKoboldOutput(`Starting ${config.name} frontend on port ${config.port}...`);
|
sendKoboldOutput(`Starting ${config.name} frontend on port ${config.port}...`);
|
||||||
|
|
||||||
|
createProxyServer(config.port, config.proxyPort);
|
||||||
|
|
||||||
const sillyTavernDataDir = getSillyTavernDataDir();
|
const sillyTavernDataDir = getSillyTavernDataDir();
|
||||||
|
|
||||||
const sillyTavernArgs = [
|
const sillyTavernArgs = [
|
||||||
|
|
@ -353,6 +401,10 @@ export async function startFrontend(args: string[]) {
|
||||||
|
|
||||||
sillyTavernProcess = await createNpxProcess(sillyTavernArgs);
|
sillyTavernProcess = await createNpxProcess(sillyTavernArgs);
|
||||||
|
|
||||||
|
if (sillyTavernProcess.pid) {
|
||||||
|
void saveSillyTavernPid(sillyTavernProcess.pid);
|
||||||
|
}
|
||||||
|
|
||||||
if (sillyTavernProcess.stdout) {
|
if (sillyTavernProcess.stdout) {
|
||||||
sillyTavernProcess.stdout.on('data', (data: Buffer) => {
|
sillyTavernProcess.stdout.on('data', (data: Buffer) => {
|
||||||
sendKoboldOutput(data.toString(), true);
|
sendKoboldOutput(data.toString(), true);
|
||||||
|
|
@ -371,6 +423,7 @@ export async function startFrontend(args: string[]) {
|
||||||
: `SillyTavern exited with code ${code}`;
|
: `SillyTavern exited with code ${code}`;
|
||||||
sendKoboldOutput(message);
|
sendKoboldOutput(message);
|
||||||
sillyTavernProcess = null;
|
sillyTavernProcess = null;
|
||||||
|
void clearSillyTavernPid();
|
||||||
});
|
});
|
||||||
|
|
||||||
sillyTavernProcess.on('error', (error) => {
|
sillyTavernProcess.on('error', (error) => {
|
||||||
|
|
@ -381,12 +434,12 @@ export async function startFrontend(args: string[]) {
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitForSillyTavernToStart();
|
await waitForSillyTavernToStart();
|
||||||
createProxyServer(config.port, config.proxyPort);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(
|
logError(
|
||||||
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
error as Error,
|
error as Error,
|
||||||
);
|
);
|
||||||
|
await stopFrontend();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -410,4 +463,10 @@ export async function stopFrontend() {
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,10 @@ async function getLinuxGPUData() {
|
||||||
|
|
||||||
async function getWindowsGPUData() {
|
async function getWindowsGPUData() {
|
||||||
try {
|
try {
|
||||||
const graphics = await siGraphics();
|
const graphics = await Promise.race([
|
||||||
|
siGraphics(),
|
||||||
|
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
||||||
|
]);
|
||||||
|
|
||||||
const discreteControllers = graphics.controllers.filter(
|
const discreteControllers = graphics.controllers.filter(
|
||||||
(controller) => controller.vram && controller.vram >= 1024,
|
(controller) => controller.vram && controller.vram >= 1024,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,16 @@ async function killWindowsProcessTree(pid: number) {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function killProcessByPid(pid: number) {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
await killWindowsProcessTree(pid);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGKILL');
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function terminateProcess(childProcess: ChildProcess | null) {
|
export async function terminateProcess(childProcess: ChildProcess | null) {
|
||||||
if (!childProcess?.pid) {
|
if (!childProcess?.pid) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue