diff --git a/packages/devkit/src/util.ts b/packages/devkit/src/util.ts index 76dd61ff25..b1bea25013 100644 --- a/packages/devkit/src/util.ts +++ b/packages/devkit/src/util.ts @@ -247,14 +247,6 @@ export function initEnv() { process.env.APP_BASE_URL = `http://127.0.0.1:${process.env.APP_PORT}`; } - if ( - !process.env.TEGO_RUNTIME_HOME && - !process.env.TEGO_RUNTIME_NAME && - _existsSync(resolve(process.cwd(), 'storage')) - ) { - process.env.TEGO_RUNTIME_HOME = process.cwd(); - } - for (const key in env) { if (!process.env[key]) { // @ts-ignore diff --git a/packages/globals/src/settings.d.ts b/packages/globals/src/settings.d.ts index 1d573f91f7..32d62e2e0b 100644 --- a/packages/globals/src/settings.d.ts +++ b/packages/globals/src/settings.d.ts @@ -12,7 +12,7 @@ export interface Settings { }; logger: { /** console | file | dailyRotateFile */ - transport: ('console' | 'file' | 'dailyRotateFile')[]; + transport?: ('console' | 'file' | 'dailyRotateFile')[]; basePath: string; level?: 'error' | 'warn' | 'info' | 'debug'; maxFiles?: string | number; // e.g. "14d" diff --git a/packages/tego/src/__tests__/env b/packages/tego/src/__tests__/env new file mode 100644 index 0000000000..88804453f5 --- /dev/null +++ b/packages/tego/src/__tests__/env @@ -0,0 +1,92 @@ +################# TACHYBASE APPLICATION ################# +APP_ENV=development +APP_PORT=3000 +APP_KEY=test-key + +# experimental support +EXTENSION_UI_BASE_PATH=/adapters/ + +API_BASE_PATH=/api/ +API_BASE_URL= + +# console | file | dailyRotateFile +LOGGER_TRANSPORT= +LOGGER_BASE_PATH=storage/logs +# error | warn | info | debug +LOGGER_LEVEL= +# If LOGGER_TRANSPORT is dailyRotateFile and using days, add 'd' as the suffix. +LOGGER_MAX_FILES= +# add 'k', 'm', 'g' as the suffix. +LOGGER_MAX_SIZE= +# json | splitter, split by '|' character +LOGGER_FORMAT= + +################# DATABASE ################# + +DB_DIALECT=sqlite +DB_STORAGE=storage/db/tachybase.sqlite +DB_TABLE_PREFIX= +# DB_HOST=localhost +# DB_PORT=5432 +# DB_DATABASE=postgres +# DB_USER=tachybase +# DB_PASSWORD=tachybase +# DB_LOGGING=on +# DB_UNDERSCORED=false + +#== SSL CONFIG ==# +# DB_DIALECT_OPTIONS_SSL_CA= +# DB_DIALECT_OPTIONS_SSL_KEY= +# DB_DIALECT_OPTIONS_SSL_CERT= +# DB_DIALECT_OPTIONS_SSL_REJECT_UNAUTHORIZED=true + +################# CACHE ################# +CACHE_DEFAULT_STORE=memory +# max number of items in memory cache +CACHE_MEMORY_MAX=2000 +# CACHE_REDIS_URL= + +################# STORAGE (Initialization only) ################# + +INIT_APP_LANG=en-US +INIT_ROOT_EMAIL=admin@tachybase.com +INIT_ROOT_PASSWORD=!Admin123. +INIT_ROOT_NICKNAME=Super Admin +INIT_ROOT_USERNAME=tachybase + +################# ENCRYPTION FIELD ################# + +ENCRYPTION_FIELD_KEY= +##### PRESETS ##### + +# 单写名称:添加指定插件且默认启用 名称前加!:移除指定插件 名称前加|:添加指定插件但默认禁用 +# PRESETS_CORE_PLUGINS=api-doc,api-keys,!messages +# PRESETS_LOCAL_PLUGINS=gantt,!iframe-block,|audit-logs +PRESETS_BULTIN_PLUGINS=acl,app-info,auth,backup,cloud-component,collection,cron,data-source,error-handler,event-source,file,workflow,message,pdf,ui-schema,user,web,worker-thread,env-secrets +PRESETS_EXTERNAL_PLUGINS=action-bulk-edit,action-bulk-update,action-custom-request,action-duplicate,action-export,action-import,action-print,block-calendar,block-charts,block-gantt,block-kanban,block-presentation,field-china-region,field-formula,field-sequence,field-encryption,log-viewer,otp,instrumentation,full-text-search,password-policy,auth-pages,manual-notification,auth-main-app,!adapter-bullmq,!adapter-red-node,!adapter-remix,!api-keys,!audit-logs,!auth-cas,!auth-dingtalk,!auth-lark,!auth-oidc,!auth-saml,!auth-sms,!auth-wechat,!auth-wecom,!block-comments,!block-map,!block-step-form,!data-source-common,!demos-game-runesweeper,!devtools,!field-markdown-vditor,!field-snapshot,!hera,!i18n-editor,!multi-app,!multi-app-share-collection,!online-user,!simple-cms,!sub-accounts,!theme-editor,!workflow-approval,!ai-chat,!department,!workflow-analysis,!api-logs,!ocr-convert,!text-copy,!user-manual-feishu,!evaluator-mathjs + + +# 主应用工作线程默认数量 +WORKER_COUNT=1 +# WORKER_TIMEOUT=1800 +# 主应用工作线程最大数量 +WORKER_COUNT_MAX=8 +# WORKER_ERROR_RETRY=3 +# 子应用工作线程默认数量 +WORKER_COUNT_SUB=0 +# 子应用工作线程最大数量 +WORKER_COUNT_MAX_SUB=1 + +# export config, max length of export data to use main thread and page size in worker thread +# EXPORT_LENGTH_MAX=2000 +# EXPORT_WORKER_PAGESIZE=1000 + +# 开发环境测试locale 强制使用 cache +#FORCE_LOCALE_CACHE=1 + +# 禁止子应用装载的插件,多个用逗号分隔 +# FORBID_SUB_APP_PLUGINS=multi-app,manual-notification,multi-app-share-collection + + +# 工作线程最大内存,单位为MB +# WORKER_MAX_MEMORY=4096 diff --git a/packages/tego/src/__tests__/utils.test.ts b/packages/tego/src/__tests__/utils.test.ts index 980055897b..51db7fef7d 100644 --- a/packages/tego/src/__tests__/utils.test.ts +++ b/packages/tego/src/__tests__/utils.test.ts @@ -15,10 +15,8 @@ describe('convertEnvToSettings', () => { const result = convertEnvToSettings(input as any); expect(result.logger.transport).toEqual(['console', 'dailyRotateFile']); - expect(result.logger.max_files).toBeUndefined(); // 未提供 expect(result.logger.maxFiles).toBe('7d'); expect(result.database.storage).toBe('storage/db/tachybase.sqlite'); - expect(result.cache.default_store).toBe('memory'); - expect(result.env.INIT_APP_LANG).toBe('zh-CN'); + expect(result.cache.defaultStore).toBe('memory'); }); }); diff --git a/packages/tego/src/constants.ts b/packages/tego/src/constants.ts index cdb4cfc75b..80616f2314 100644 --- a/packages/tego/src/constants.ts +++ b/packages/tego/src/constants.ts @@ -2,17 +2,25 @@ import fs from 'node:fs'; import path from 'node:path'; import TachybaseGlobal from '@tachybase/globals'; -import { parseEnvironment } from './utils'; +import { convertEnvToSettings, parseEnvironment } from './utils'; // 解析环境变量 parseEnvironment(); // 读取配置 -if (!fs.existsSync(`${process.env.TEGO_RUNTIME_HOME}/settings.js`)) { - fs.mkdirSync(`${process.env.TEGO_RUNTIME_HOME}`, { recursive: true }); - fs.copyFileSync(path.join(__dirname, '../presets/settings.js'), `${process.env.TEGO_RUNTIME_HOME}/settings.js`); +// 兼容旧的 .env 环境变量文件作为配置(仅在 settings.js 不存在时使用,且 .env 文件存在) +if ( + fs.existsSync(path.join(process.env.TEGO_RUNTIME_HOME, '.env')) && + !fs.existsSync(`${process.env.TEGO_RUNTIME_HOME}/settings.js`) +) { + if (!fs.existsSync(`${process.env.TEGO_RUNTIME_HOME}/settings.js`)) { + fs.mkdirSync(`${process.env.TEGO_RUNTIME_HOME}`, { recursive: true }); + fs.copyFileSync(path.join(__dirname, '../presets/settings.js'), `${process.env.TEGO_RUNTIME_HOME}/settings.js`); + } + TachybaseGlobal.settings = require(`${process.env.TEGO_RUNTIME_HOME}/settings.js`); +} else { + TachybaseGlobal.settings = convertEnvToSettings(process.env); } -TachybaseGlobal.settings = require(`${process.env.TEGO_RUNTIME_HOME}/settings.js`); for (const key in TachybaseGlobal.settings.env) { if (!process.env[key]) { diff --git a/packages/tego/src/utils.ts b/packages/tego/src/utils.ts index 078a6baaf6..07546e5790 100644 --- a/packages/tego/src/utils.ts +++ b/packages/tego/src/utils.ts @@ -9,12 +9,14 @@ import { access, mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises import os from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { pipeline } from 'node:stream/promises'; -import TachybaseGlobal from '@tachybase/globals'; +import TachybaseGlobal, { Settings } from '@tachybase/globals'; import { config } from 'dotenv'; +import { cloneDeep } from 'lodash'; import npmRegistryFetch from 'npm-registry-fetch'; import * as tar from 'tar'; +import defaultSettings from '../presets/settings'; import { DEFAULT_WEB_PACKAGE_NAME, LAST_UPDATE_FILE_SUFFIX } from './constants'; export function parseEnvironment() { @@ -30,15 +32,6 @@ export function parseEnvironment() { }); } - // 如果存在 storage 的话,TEGO_RUNTIME_HOME 默认指向当前路径 - if ( - !process.env.TEGO_RUNTIME_HOME && - !process.env.TEGO_RUNTIME_NAME && - fs.existsSync(resolve(process.cwd(), 'storage')) - ) { - process.env.TEGO_RUNTIME_HOME = process.cwd(); - } - for (const key in env) { if (!process.env[key]) { process.env[key] = env[key]; @@ -209,62 +202,138 @@ export class TegoIndexManager { } export function convertEnvToSettings(flatEnv: Record) { - const settings: any = { - env: {}, - logger: {}, - database: {}, - cache: {}, - encryptionField: {}, - presets: {}, - worker: {}, - export: {}, - misc: {}, - }; - - // LOGGER_ - for (const key in flatEnv) { - const value = flatEnv[key]; - if (value === undefined) continue; - - if (key.startsWith('LOGGER_')) { - const subKey = key.replace('LOGGER_', '').toLowerCase(); - if (subKey === 'transport') { - settings.logger.transport = value.split(',').map((x) => x.trim()); - } else if (subKey === 'maxfiles') { + const settings: Settings = cloneDeep(defaultSettings); + + for (const [key, value] of Object.entries(flatEnv)) { + if (!value) continue; + + switch (key) { + /** ================= LOGGER ================= */ + case 'LOGGER_TRANSPORT': + settings.logger.transport = value.split(',') as any; + break; + case 'LOGGER_BASE_PATH': + settings.logger.basePath = value; + break; + case 'LOGGER_LEVEL': + settings.logger.level = value as any; + break; + case 'LOGGER_MAX_FILES': settings.logger.maxFiles = value; - } else if (subKey === 'maxsize') { + break; + case 'LOGGER_MAX_SIZE': settings.logger.maxSize = value; - } else if (subKey === 'format') { - settings.logger.format = value; - } else { - settings.logger[subKey] = value; - } - continue; - } - - // DB_ - if (key.startsWith('DB_')) { - const subKey = key.replace('DB_', '').toLowerCase(); - if (subKey.startsWith('dialect_options_ssl_')) { - // e.g. DB_DIALECT_OPTIONS_SSL_CA - const sslKey = subKey.replace('dialect_options_ssl_', ''); + break; + case 'LOGGER_FORMAT': + settings.logger.format = value as any; + break; + + /** ================= DATABASE ================= */ + case 'DB_DIALECT': + settings.database.dialect = value as any; + break; + case 'DB_STORAGE': + settings.database.storage = value; + break; + case 'DB_HOST': + settings.database.host = value; + break; + case 'DB_PORT': + settings.database.port = +value; + break; + case 'DB_DATABASE': + settings.database.database = value; + break; + case 'DB_USER': + settings.database.user = value; + break; + case 'DB_PASSWORD': + settings.database.password = value; + break; + case 'DB_LOGGING': + settings.database.logging = value === 'on'; + break; + case 'DB_TABLE_PREFIX': + settings.database.tablePrefix = value; + break; + case 'DB_UNDERSCORED': + settings.database.underscored = value === 'true'; + break; + + case 'DB_DIALECT_OPTIONS_SSL_CA': settings.database.ssl = settings.database.ssl || {}; - settings.database.ssl[sslKey] = value; - } else { - settings.database[subKey] = value; - } - continue; - } - - // CACHE_ - if (key.startsWith('CACHE_')) { - const subKey = key.replace('CACHE_', '').toLowerCase(); - settings.cache[subKey] = value; - continue; + settings.database.ssl.ca = value; + break; + case 'DB_DIALECT_OPTIONS_SSL_KEY': + settings.database.ssl = settings.database.ssl || {}; + settings.database.ssl.key = value; + break; + case 'DB_DIALECT_OPTIONS_SSL_CERT': + settings.database.ssl = settings.database.ssl || {}; + settings.database.ssl.cert = value; + break; + case 'DB_DIALECT_OPTIONS_SSL_REJECT_UNAUTHORIZED': + settings.database.ssl = settings.database.ssl || {}; + settings.database.ssl.rejectUnauthorized = value === 'true'; + break; + + /** ================= CACHE ================= */ + case 'CACHE_DEFAULT_STORE': + settings.cache.defaultStore = value as any; + break; + case 'CACHE_MEMORY_MAX': + settings.cache.memoryMax = +value; + break; + case 'CACHE_REDIS_URL': + settings.cache.redisUrl = value; + break; + + /** ================= ENCRYPTION ================= */ + case 'ENCRYPTION_FIELD_KEY': + settings.encryptionField.key = value; + break; + + /** ================= PRESETS ================= */ + case 'PRESETS_BULTIN_PLUGINS': + settings.presets.builtinPlugins = value.split(','); + break; + case 'PRESETS_EXTERNAL_PLUGINS': + settings.presets.externalPlugins = value.split(',').map((name) => { + if (name.startsWith('!')) { + return { name: name.slice(1), enabledByDefault: false }; + } + if (name.startsWith('|')) { + return { name: name.slice(1), enabledByDefault: false }; + } + return { name, enabledByDefault: true }; + }); + break; + + /** ================= WORKER ================= */ + case 'WORKER_COUNT': + settings.worker.count = +value; + break; + case 'WORKER_COUNT_MAX': + settings.worker.countMax = +value; + break; + + /** ================= EXPORT ================= */ + case 'EXPORT_LENGTH_MAX': + settings.export.lengthMax = +value; + break; + case 'EXPORT_WORKER_PAGESIZE': + settings.export.workerPageSize = +value; + break; + + /** ================= MISC ================= */ + case 'FORBID_SUB_APP_PLUGINS': + settings.misc.forbidSubAppPlugins = value.split(','); + break; + + default: + // 不处理未知 key + break; } - - // 其它 => 默认归到 settings.env - settings.env[key] = value; } return settings;