[config] Add runtimeConfig config property; Fix bug in aiProxyHandler.js

This commit is contained in:
Sergey Konovalov
2025-06-06 14:14:23 +03:00
parent a00409a01e
commit 0239d30422
13 changed files with 108 additions and 62 deletions

View File

@ -30,6 +30,14 @@
"replaceConsole": true
}
},
"runtimeConfig": {
"filePath": "",
"cache": {
"stdTTL": 300,
"checkperiod": 60,
"useClones": false
}
},
"queue": {
"type": "rabbitmq",
"visibilityTimeout": 300,

View File

@ -2,6 +2,9 @@
"log": {
"filePath": "../Common/config/log4js/development.json"
},
"runtimeConfig": {
"filePath": "./../runtime.json"
},
"queue": {
"visibilityTimeout": 900
},

View File

@ -2,6 +2,9 @@
"log": {
"filePath": "../Common/config/log4js/development.json"
},
"runtimeConfig": {
"filePath": "./../runtime.json"
},
"queue": {
"visibilityTimeout": 900
},

View File

@ -2,6 +2,9 @@
"log": {
"filePath": "../Common/config/log4js/development.json"
},
"runtimeConfig": {
"filePath": "./../runtime.json"
},
"queue": {
"visibilityTimeout": 900
},

View File

@ -5,6 +5,9 @@
"aiSettings": {
"pluginDir" : "/var/www/onlyoffice/documentserver/server/info/ai"
},
"runtimeConfig": {
"filePath": "/var/www/onlyoffice/documentserver/../Data/runtime.json"
},
"storage": {
"fs": {
"folderPath": "/var/lib/onlyoffice/documentserver/App_Data/cache/files"

View File

@ -2,6 +2,9 @@
"log": {
"filePath": "../../config/log4js/production.json"
},
"runtimeConfig": {
"filePath": "./../runtime.json"
},
"aiSettings": {
"pluginDir" : "../info/ai"
},

View File

@ -32,10 +32,12 @@
'use strict';
const config = require('config');
const utils = require('./utils');
const logger = require('./logger');
const constants = require('./constants');
const tenantManager = require('./tenantManager');
const runtimeConfigManager = require('./runtimeConfigManager');
function Context(){
this.logger = logger.getLogger('nodeJS');
@ -85,7 +87,10 @@ Context.prototype.initFromPubSub = function(data) {
this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc);
};
Context.prototype.initTenantCache = async function() {
this.config = await tenantManager.getTenantConfig(this);
const runtimeConfig = await runtimeConfigManager.getConfig(this);
const tenantConfig = await tenantManager.getTenantConfig(this);
this.config = utils.deepMergeObjects({}, runtimeConfig, tenantConfig);
//todo license and secret
};
@ -122,6 +127,13 @@ Context.prototype.getCfg = function(property, defaultValue) {
}
return defaultValue;
};
/**
* Get the full configuration by combining system config with context config
* @returns {object} The merged configuration object
*/
Context.prototype.getFullCfg = function() {
return {...config.util.toObject(), ...this.config};
};
/**
* Underlying get mechanism

View File

@ -39,7 +39,7 @@ const license = require('./../../Common/sources/license');
const constants = require('./../../Common/sources/constants');
const commonDefines = require('./../../Common/sources/commondefines');
const utils = require('./../../Common/sources/utils');
const { readFile, readdir } = require('fs/promises');
const { readFile, readdir, writeFile } = require('fs/promises');
const path = require('path');
const cfgTenantsBaseDomain = config.get('tenants.baseDomain');
@ -123,6 +123,22 @@ async function getTenantConfig(ctx) {
}
return res;
}
/**
* Set tenant configuration for the current context
* @param {operationContext} ctx - Operation context
* @param {Object} config - Configuration data to save
* @returns {Object} Saved configuration object
*/
async function setTenantConfig(ctx, config) {
let newConfig = await getTenantConfig(ctx);
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
newConfig = {...newConfig, ...config};
await writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
nodeCache.set(configPath, newConfig);
}
return newConfig;
}
function getTenantSecret(ctx, type) {
return co(function*() {
let cfgTenant;
@ -428,6 +444,7 @@ exports.getTenantSecret = getTenantSecret;
exports.getTenantLicense = getTenantLicense;
exports.getServerLicense = getServerLicense;
exports.setDefLicense = setDefLicense;
exports.setTenantConfig = setTenantConfig;
exports.isMultitenantMode = isMultitenantMode;
exports.setMultitenantMode = setMultitenantMode;
exports.isDefaultTenant = isDefaultTenant;

View File

@ -47,6 +47,7 @@ const cfgAiApiAllowedOrigins = config.get('aiSettings.allowedCorsOrigins');
const cfgAiApiTimeout = config.get('aiSettings.timeout');
const cfgAiApiCache = config.get('aiSettings.cache');
const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser');
const cfgAiSettings = config.get('aiSettings');
const AI = aiEngine.AI;
const nodeCache = new utils.NodeCache(cfgAiApiCache);
@ -113,6 +114,7 @@ async function proxyRequest(req, res) {
try {
ctx.logger.info('Start proxyRequest');
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
const tenAiApi = ctx.getCfg('aiSettings', cfgAiSettings);
// 1. Handle CORS preflight (OPTIONS) requests if necessary
if (handleCorsHeaders(req, res, ctx) === true) {
@ -146,6 +148,10 @@ async function proxyRequest(req, res) {
// Find the provider that matches the target URL
for (let providerName in AI.Providers) {//todo try for of
if (body.target.includes(AI.Providers[providerName].url)) {
if (tenAiApi?.providers?.[providerName]) {
AI.Providers[providerName].key = tenAiApi.providers[providerName].key;
AI.Providers[providerName].url = tenAiApi.providers[providerName].url;
}
providerHeaders = AI._getHeaders(AI.Providers[providerName]);
break;
}
@ -279,7 +285,8 @@ async function getPluginSettings(ctx) {
};
try {
// Get AI API configuration
const aiApi = config.get('aiSettings');
const tenAiApi = ctx.getCfg('aiSettings', cfgAiSettings);
return tenAiApi;
// Process providers and their models if configuration exists
if (aiApi?.providers && typeof aiApi.providers === 'object') {
const providers = AI.serializeProviders();
@ -347,11 +354,13 @@ async function requestModels(req, res) {
try {
await ctx.initTenantCache();
let body = JSON.parse(req.body);
if (body.key && AI.Providers[body.name]) {
if (AI.Providers[body.name]) {
AI.Providers[body.name].key = body.key;
AI.Providers[body.name].url = body.url;
}
let models = await AI.getModels(body);
res.json(models);
let getRes = await AI.getModels(body);
getRes.modelsApi = AI.TmpProviderForModels?.models;
res.json(getRes);
} catch (error) {
ctx.logger.error('getModels error: %s', error.stack);
res.sendStatus(400);

View File

@ -31,13 +31,11 @@
*/
const config = require('config');
const { readFile, writeFile, stat, cp } = require('fs/promises');
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const tenantManager = require('../../../Common/sources/tenantManager');
const operationContext = require('../../../Common/sources/operationContext');
const aiProxyHandler = require('../ai/aiProxyHandler');
const runtimeConfigManager = require('../../../Common/sources/runtimeConfigManager');
const router = express.Router();
@ -51,16 +49,8 @@ router.get('/', async (req, res) => {
ctx.initFromRequest(req);
await ctx.initTenantCache();
ctx.logger.debug('config get start');
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
//todo
}
let configPath = path.join(process.env.NODE_CONFIG_DIR, 'local.json');
try {
result = await readFile(configPath, {encoding: 'utf8'});
} catch (e) {
ctx.logger.debug('config get error: %s', e.stack);
}
let cfg = ctx.getFullCfg();
result = JSON.stringify(cfg);
} catch (error) {
ctx.logger.error('config get error: %s', error.stack);
}
@ -76,32 +66,15 @@ router.post('/', rawFileParser, async (req, res) => {
try {
ctx.initFromRequest(req);
await ctx.initTenantCache();
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
//todo
}
let newConfig = JSON.parse(req.body);
// Define file paths
let configPath = path.join(process.env.NODE_CONFIG_DIR, 'local.json');
let backupPath = path.join(process.env.NODE_CONFIG_DIR, 'local.json.bak');
// Create backup of current config before saving
let sampleFileStat = null;
try {
sampleFileStat = await stat(backupPath);
} catch (backupError) {
ctx.logger.debug('Configuration backup not found: %s', backupError.stack);
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
await tenantManager.setTenantConfig(ctx, newConfig);
} else {
await runtimeConfigManager.saveConfig(ctx, newConfig);
}
if(!sampleFileStat){
await cp(configPath, backupPath, {force: true, recursive: true});
}
try {
const oldConfig = JSON.parse(await readFile(configPath, {encoding: 'utf8'}));
newConfig = {...oldConfig, ...newConfig};
} catch (error) {
ctx.logger.debug('Configuration local.json not found: %s', error.stack);
}
const prettyConfig = JSON.stringify(newConfig, null, 2);
await writeFile(configPath, prettyConfig, {encoding: 'utf8'});
res.sendStatus(200);
} catch (error) {
ctx.logger.error('Configuration save error: %s', error.stack);

View File

@ -22,7 +22,7 @@ body {
#actions-list {
position: relative;
height: 230px;
height: 350px;
margin-top: 32px;
}

View File

@ -229,6 +229,10 @@ const AIIntegration = {
goBack() {
this.navigateToView('settings');
if (this.onBack) {
this.onBack();
}
},
ok() {

View File

@ -36,6 +36,7 @@
var settings = null;
var framesToInit = [];
var tempModels = null;
var urlSettings = 'plugin/settings';
var urlModels = 'plugin/models';
var urlConfig = 'config';
@ -53,15 +54,7 @@
}
});
AIIntegration.onSave = function() {
var settingsFiltered = Object.assign({}, settings);
if (settingsFiltered.providers) {
for (var id in settingsFiltered.providers) {
if (settingsFiltered.providers.hasOwnProperty(id)) {
settingsFiltered.providers[id].models = [];
}
}
}
var config = {aiSettings: settingsFiltered};
var config = {aiSettings: settings};
return putConfig(config).then(function() {
return true;
}).catch(function() {
@ -77,6 +70,13 @@
}
return;
};
AIIntegration.onBack = function() {
var settingsWindow = findIframeBySrcPart('settings');
if(settingsWindow) {
updateActions(settingsWindow.contentWindow);
updateModels();
}
};
});
function onInit(source) {
@ -183,12 +183,6 @@
var aiModelEditWindow = findIframeBySrcPart('aiModelEdit');
if(aiModelEditWindow) {
const providers = Object.keys(settings.providers).map(function(key) { return settings.providers[key]; });
sendMessageToSettings({
name: 'onProvidersUpdate',
data: providers
}, aiModelEditWindow.contentWindow);
var model = {id: "", name: "", provider: "", capabilities: 0};
if (message.data.model) {
model = settings.models.find(function(model) { return model.name === message.data.model.name; });
@ -197,6 +191,13 @@
model : model,
providers : providers
}
sendMessageToSettings({
name: 'onProvidersUpdate',
data: data
}, aiModelEditWindow.contentWindow);
sendMessageToSettings({
name: 'onModelInfo',
data: data
@ -209,9 +210,10 @@
}
break;
case 'onDeleteAiModel':
for (let id in settings.models) {
if (settings.models[id].id == message.data.id) {
delete settings.models[id];
for (var i = 0; i < settings.models.length; i++) {
if (settings.models[i].id == message.data.id) {
settings.models.splice(i, 1);
break;
}
}
updateModels();
@ -291,6 +293,8 @@
// console.log('Configuration saved successfully');
return response.json();
}).then(function(models) {
tempModels = models.modelsApi;
delete models.modelsApi;
sendMessageToSettings({
name: 'onGetModels',
data: models
@ -300,6 +304,10 @@
function onChangeModel(data) {
settings.providers[data.provider.name] = data.provider;
if (tempModels) {
settings.providers[data.provider.name].models = tempModels;
tempModels = null;
}
let isFoundModel = false;
for(let id in settings.models) {