From fb2ac07b887bba2c4753bde1b8545fc5f0363d9b Mon Sep 17 00:00:00 2001 From: sealday Date: Wed, 6 Aug 2025 22:42:33 +0800 Subject: [PATCH 1/3] chore: add input env file --- packages/tego/src/__tests__/env | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/tego/src/__tests__/env diff --git a/packages/tego/src/__tests__/env b/packages/tego/src/__tests__/env new file mode 100644 index 0000000000..c72af750e5 --- /dev/null +++ b/packages/tego/src/__tests__/env @@ -0,0 +1,112 @@ +################# 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= + +################# TELEMETRY ################# + +# TELEMETRY_ENABLED=on # 是否启用后端遥测 +# TELEMETRY_SERVICE_NAME=tachybase-default-demo # 服务名称,默认为 tachybase-main +# OTEL_LOG_LEVEL=debug # OpenTelemetry 日志级别,仅输出到控制台,见 https://open-telemetry.github.io/opentelemetry-js/enums/_opentelemetry_api.DiagLogLevel.html ,不配置则默认不输出日志 +# OTEL_METRICS_READER=console,prometheus # 指标读取器,两个均为内置,console 为控制台,prometheus 为 prometheus 服务器 +# OTEL_PROMETHEUS_SERVER=on # 是否启用 Prometheus 服务,用于创建服务器以供导出指标数据 +# OTEL_PROMETHEUS_PORT=9464 # Prometheus 服务器端口,默认为 9464,注意此服务器没有鉴权 +# OTEL_TRACES_PROCESSOR=console,otlp # 链路追踪处理器,两个均为内置,console 为控制台,otlp 为 OTLP 规范的追踪服务器 +# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_GRPC=http://localhost:4317 # OTLP https://opentelemetry.io/docs/specs/otlp/ 规范的追踪服务器的 gRPC 端点地址 +# 下面为前端 Sentry 跟踪配置,注意修改后需要重新 build 前端代码 +# SENTRY_DSN= # Sentry DSN,用于前端遥测,留空则不启用前端遥测 +# SENTRY_TRACE_ENABLE=on # 是否启用 Sentry 跟踪,文档 https://docs.sentry.io/platforms/javascript/guides/react/tracing/ +# SENTRY_TRACE_SAMPLE_RATE=1.0 # Sentry 跟踪采样率,1.0 为 100%,0.0 为 0%,默认为 1.0 +# SENTRY_TRACE_PROPAGATION_TARGETS=localhost,/^\// # Sentry 跟踪传播目标过滤器,多个用逗号分隔,支持正则,默认为 localhost,/^\// ,文档 https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#tracepropagationtargets +# SENTRY_SESSION_REPLAY_ENABLE=on # 是否启用 Sentry 会话重放,文档 https://docs.sentry.io/platforms/javascript/guides/react/session-replay/ +# SENTRY_SESSION_REPLAY_SAMPLE_RATE=0.1 # Sentry 会话重放采样率,1.0 为 100%,0.0 为 0%,默认为 0.1 +# SENTRY_SESSION_REPLAY_ONERROR_SAMPLE_RATE=1.0 # Sentry 会话重放错误采样率,1.0 为 100%,0.0 为 0%,默认为 1.0 + +##### 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 From 0f5d8a542151e36eec2ff58e875bd262b303c88d Mon Sep 17 00:00:00 2001 From: sealday Date: Tue, 19 Aug 2025 08:12:30 +0800 Subject: [PATCH 2/3] feat: convert old env to settings object --- packages/globals/src/settings.d.ts | 2 +- packages/tego/src/__tests__/env | 20 --- packages/tego/src/__tests__/utils.test.ts | 4 +- packages/tego/src/utils.ts | 184 +++++++++++++++------- 4 files changed, 133 insertions(+), 77 deletions(-) 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 index c72af750e5..88804453f5 100644 --- a/packages/tego/src/__tests__/env +++ b/packages/tego/src/__tests__/env @@ -57,26 +57,6 @@ INIT_ROOT_USERNAME=tachybase ################# ENCRYPTION FIELD ################# ENCRYPTION_FIELD_KEY= - -################# TELEMETRY ################# - -# TELEMETRY_ENABLED=on # 是否启用后端遥测 -# TELEMETRY_SERVICE_NAME=tachybase-default-demo # 服务名称,默认为 tachybase-main -# OTEL_LOG_LEVEL=debug # OpenTelemetry 日志级别,仅输出到控制台,见 https://open-telemetry.github.io/opentelemetry-js/enums/_opentelemetry_api.DiagLogLevel.html ,不配置则默认不输出日志 -# OTEL_METRICS_READER=console,prometheus # 指标读取器,两个均为内置,console 为控制台,prometheus 为 prometheus 服务器 -# OTEL_PROMETHEUS_SERVER=on # 是否启用 Prometheus 服务,用于创建服务器以供导出指标数据 -# OTEL_PROMETHEUS_PORT=9464 # Prometheus 服务器端口,默认为 9464,注意此服务器没有鉴权 -# OTEL_TRACES_PROCESSOR=console,otlp # 链路追踪处理器,两个均为内置,console 为控制台,otlp 为 OTLP 规范的追踪服务器 -# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_GRPC=http://localhost:4317 # OTLP https://opentelemetry.io/docs/specs/otlp/ 规范的追踪服务器的 gRPC 端点地址 -# 下面为前端 Sentry 跟踪配置,注意修改后需要重新 build 前端代码 -# SENTRY_DSN= # Sentry DSN,用于前端遥测,留空则不启用前端遥测 -# SENTRY_TRACE_ENABLE=on # 是否启用 Sentry 跟踪,文档 https://docs.sentry.io/platforms/javascript/guides/react/tracing/ -# SENTRY_TRACE_SAMPLE_RATE=1.0 # Sentry 跟踪采样率,1.0 为 100%,0.0 为 0%,默认为 1.0 -# SENTRY_TRACE_PROPAGATION_TARGETS=localhost,/^\// # Sentry 跟踪传播目标过滤器,多个用逗号分隔,支持正则,默认为 localhost,/^\// ,文档 https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#tracepropagationtargets -# SENTRY_SESSION_REPLAY_ENABLE=on # 是否启用 Sentry 会话重放,文档 https://docs.sentry.io/platforms/javascript/guides/react/session-replay/ -# SENTRY_SESSION_REPLAY_SAMPLE_RATE=0.1 # Sentry 会话重放采样率,1.0 为 100%,0.0 为 0%,默认为 0.1 -# SENTRY_SESSION_REPLAY_ONERROR_SAMPLE_RATE=1.0 # Sentry 会话重放错误采样率,1.0 为 100%,0.0 为 0%,默认为 1.0 - ##### PRESETS ##### # 单写名称:添加指定插件且默认启用 名称前加!:移除指定插件 名称前加|:添加指定插件但默认禁用 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/utils.ts b/packages/tego/src/utils.ts index 078a6baaf6..5ee919664b 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() { @@ -209,62 +211,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; From e1829df86bae0fb4fd5c020460b4a00cfea36ccb Mon Sep 17 00:00:00 2001 From: sealday Date: Tue, 19 Aug 2025 08:30:32 +0800 Subject: [PATCH 3/3] fix: settings --- packages/devkit/src/util.ts | 8 -------- packages/tego/src/constants.ts | 18 +++++++++++++----- packages/tego/src/utils.ts | 9 --------- 3 files changed, 13 insertions(+), 22 deletions(-) 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/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 5ee919664b..07546e5790 100644 --- a/packages/tego/src/utils.ts +++ b/packages/tego/src/utils.ts @@ -32,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];