From 6d980409d9e305cf1833cbda830df7c567061de6 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Fri, 6 Jun 2025 14:14:23 +0300 Subject: [PATCH] [config] Add runtimeConfig config property; Fix bug in aiProxyHandler.js --- Common/config/default.json | 8 ++++ Common/config/development-linux.json | 3 ++ Common/config/development-mac.json | 3 ++ Common/config/development-windows.json | 3 ++ Common/config/production-linux.json | 3 ++ Common/config/production-windows.json | 3 ++ Common/sources/operationContext.js | 14 +++++- Common/sources/tenantManager.js | 19 +++++++- DocService/sources/ai/aiProxyHandler.js | 17 +++++-- DocService/sources/routes/config.js | 47 ++++--------------- .../info/ai/resources/styles/settings.css | 2 +- branding/info/js/ai-integration.js | 4 ++ branding/info/js/ai-interface.js | 44 ++++++++++------- 13 files changed, 108 insertions(+), 62 deletions(-) diff --git a/Common/config/default.json b/Common/config/default.json index ae295652..8bdc9a2d 100644 --- a/Common/config/default.json +++ b/Common/config/default.json @@ -30,6 +30,14 @@ "replaceConsole": true } }, + "runtimeConfig": { + "filePath": "", + "cache": { + "stdTTL": 300, + "checkperiod": 60, + "useClones": false + } + }, "queue": { "type": "rabbitmq", "visibilityTimeout": 300, diff --git a/Common/config/development-linux.json b/Common/config/development-linux.json index 6e3a49b3..d6cb93ad 100644 --- a/Common/config/development-linux.json +++ b/Common/config/development-linux.json @@ -2,6 +2,9 @@ "log": { "filePath": "../Common/config/log4js/development.json" }, + "runtimeConfig": { + "filePath": "./../runtime.json" + }, "queue": { "visibilityTimeout": 900 }, diff --git a/Common/config/development-mac.json b/Common/config/development-mac.json index 12959f4c..cc512f20 100644 --- a/Common/config/development-mac.json +++ b/Common/config/development-mac.json @@ -2,6 +2,9 @@ "log": { "filePath": "../Common/config/log4js/development.json" }, + "runtimeConfig": { + "filePath": "./../runtime.json" + }, "queue": { "visibilityTimeout": 900 }, diff --git a/Common/config/development-windows.json b/Common/config/development-windows.json index 54c6abcd..db1a3032 100644 --- a/Common/config/development-windows.json +++ b/Common/config/development-windows.json @@ -2,6 +2,9 @@ "log": { "filePath": "../Common/config/log4js/development.json" }, + "runtimeConfig": { + "filePath": "./../runtime.json" + }, "queue": { "visibilityTimeout": 900 }, diff --git a/Common/config/production-linux.json b/Common/config/production-linux.json index f7eae4c6..5ae26bd2 100644 --- a/Common/config/production-linux.json +++ b/Common/config/production-linux.json @@ -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" diff --git a/Common/config/production-windows.json b/Common/config/production-windows.json index 66fea6f1..05df56aa 100644 --- a/Common/config/production-windows.json +++ b/Common/config/production-windows.json @@ -2,6 +2,9 @@ "log": { "filePath": "../../config/log4js/production.json" }, + "runtimeConfig": { + "filePath": "./../runtime.json" + }, "aiSettings": { "pluginDir" : "../info/ai" }, diff --git a/Common/sources/operationContext.js b/Common/sources/operationContext.js index 78fe402f..f35f5ae2 100644 --- a/Common/sources/operationContext.js +++ b/Common/sources/operationContext.js @@ -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 diff --git a/Common/sources/tenantManager.js b/Common/sources/tenantManager.js index 7f14dc85..5ea87925 100644 --- a/Common/sources/tenantManager.js +++ b/Common/sources/tenantManager.js @@ -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; diff --git a/DocService/sources/ai/aiProxyHandler.js b/DocService/sources/ai/aiProxyHandler.js index a706d713..4612e108 100644 --- a/DocService/sources/ai/aiProxyHandler.js +++ b/DocService/sources/ai/aiProxyHandler.js @@ -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); diff --git a/DocService/sources/routes/config.js b/DocService/sources/routes/config.js index 561972fa..54e6986a 100644 --- a/DocService/sources/routes/config.js +++ b/DocService/sources/routes/config.js @@ -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); diff --git a/branding/info/ai/resources/styles/settings.css b/branding/info/ai/resources/styles/settings.css index eedfd87d..a6364fd4 100644 --- a/branding/info/ai/resources/styles/settings.css +++ b/branding/info/ai/resources/styles/settings.css @@ -22,7 +22,7 @@ body { #actions-list { position: relative; - height: 230px; + height: 350px; margin-top: 32px; } diff --git a/branding/info/js/ai-integration.js b/branding/info/js/ai-integration.js index 688aa590..07f71e03 100644 --- a/branding/info/js/ai-integration.js +++ b/branding/info/js/ai-integration.js @@ -229,6 +229,10 @@ const AIIntegration = { goBack() { this.navigateToView('settings'); + + if (this.onBack) { + this.onBack(); + } }, ok() { diff --git a/branding/info/js/ai-interface.js b/branding/info/js/ai-interface.js index 32ca1d08..2a606661 100644 --- a/branding/info/js/ai-interface.js +++ b/branding/info/js/ai-interface.js @@ -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) {