Files
Perplexica/src/lib/config/index.ts
2025-10-17 14:36:04 +05:30

263 lines
6.2 KiB
TypeScript

import path from 'node:path';
import fs from 'fs';
import { Config, ConfigModelProvider, UIConfigSections } from './types';
import { hashObj } from '../serverUtils';
import { getModelProvidersUIConfigSection } from '../models/providers';
class ConfigManager {
configPath: string = path.join(
process.env.DATA_DIR || process.cwd(),
'/data/config.json',
);
configVersion = 1;
currentConfig: Config = {
version: this.configVersion,
setupComplete: false,
general: {},
modelProviders: [],
search: {
searxngURL: '',
},
};
uiConfigSections: UIConfigSections = {
general: [
{
name: 'Theme',
key: 'theme',
type: 'select',
options: [
{
name: 'Light',
value: 'light',
},
{
name: 'Dark',
value: 'dark',
},
],
required: false,
description: 'Choose between light and dark layouts for the app.',
default: 'dark',
scope: 'client',
},
],
modelProviders: [],
search: [
{
name: 'SearXNG URL',
key: 'searxngURL',
type: 'string',
required: false,
description: 'The URL of your SearXNG instance',
placeholder: 'http://localhost:4000',
default: '',
scope: 'server',
env: 'SEARXNG_API_URL',
},
],
};
constructor() {
this.initialize();
}
private initialize() {
this.initializeConfig();
this.initializeFromEnv();
}
private saveConfig() {
fs.writeFileSync(
this.configPath,
JSON.stringify(this.currentConfig, null, 2),
);
}
private initializeConfig() {
const exists = fs.existsSync(this.configPath);
if (!exists) {
fs.writeFileSync(
this.configPath,
JSON.stringify(this.currentConfig, null, 2),
);
} else {
try {
this.currentConfig = JSON.parse(
fs.readFileSync(this.configPath, 'utf-8'),
);
} catch (err) {
if (err instanceof SyntaxError) {
console.error(
`Error parsing config file at ${this.configPath}:`,
err,
);
console.log(
'Loading default config and overwriting the existing file.',
);
fs.writeFileSync(
this.configPath,
JSON.stringify(this.currentConfig, null, 2),
);
return;
} else {
console.log('Unknown error reading config file:', err);
}
}
this.currentConfig = this.migrateConfig(this.currentConfig);
}
}
private migrateConfig(config: Config): Config {
/* TODO: Add migrations */
return config;
}
private initializeFromEnv() {
/* providers section*/
const providerConfigSections = getModelProvidersUIConfigSection();
this.uiConfigSections.modelProviders = providerConfigSections;
const newProviders: ConfigModelProvider[] = [];
providerConfigSections.forEach((provider) => {
const newProvider: ConfigModelProvider & { required?: string[] } = {
id: crypto.randomUUID(),
name: `${provider.name} ${Math.floor(Math.random() * 1000)}`,
type: provider.key,
chatModels: [],
embeddingModels: [],
config: {},
required: [],
hash: '',
};
provider.fields.forEach((field) => {
newProvider.config[field.key] =
process.env[field.env!] ||
field.default ||
''; /* Env var must exist for providers */
if (field.required) newProvider.required?.push(field.key);
});
let configured = true;
newProvider.required?.forEach((r) => {
if (!newProvider.config[r]) {
configured = false;
}
});
if (configured) {
const hash = hashObj(newProvider.config);
newProvider.hash = hash;
delete newProvider.required;
const exists = this.currentConfig.modelProviders.find(
(p) => p.hash === hash,
);
if (!exists) {
newProviders.push(newProvider);
}
}
});
this.currentConfig.modelProviders.push(...newProviders);
/* search section */
this.uiConfigSections.search.forEach((f) => {
if (f.env && !this.currentConfig.search[f.key]) {
this.currentConfig.search[f.key] =
process.env[f.env] ?? f.default ?? '';
}
});
this.saveConfig();
}
public getConfig(key: string, defaultValue?: any): any {
const nested = key.split('.');
let obj: any = this.currentConfig;
for (let i = 0; i < nested.length; i++) {
const part = nested[i];
if (obj == null) return defaultValue;
obj = obj[part];
}
return obj === undefined ? defaultValue : obj;
}
public updateConfig(key: string, val: any) {
const parts = key.split('.');
if (parts.length === 0) return;
let target: any = this.currentConfig;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (target[part] === null || typeof target[part] !== 'object') {
target[part] = {};
}
target = target[part];
}
const finalKey = parts[parts.length - 1];
target[finalKey] = val;
this.saveConfig();
}
public addModelProvider(type: string, name: string, config: any) {
const newModelProvider: ConfigModelProvider = {
id: crypto.randomUUID(),
name,
type,
config,
chatModels: [],
embeddingModels: [],
hash: hashObj(config),
};
this.currentConfig.modelProviders.push(newModelProvider);
this.saveConfig();
}
public removeModelProvider(id: string) {
const index = this.currentConfig.modelProviders.findIndex(
(p) => p.id === id,
);
if (index === -1) return;
this.currentConfig.modelProviders =
this.currentConfig.modelProviders.filter((p) => p.id !== id);
this.saveConfig();
}
public isSetupComplete() {
return this.currentConfig.setupComplete;
}
public markSetupComplete() {
if (!this.currentConfig.setupComplete) {
this.currentConfig.setupComplete = true;
}
this.saveConfig();
}
public getUIConfigSections(): UIConfigSections {
return this.uiConfigSections;
}
}
const configManager = new ConfigManager();
export default configManager;