From 728dd54e99fef1b20ddb89845fa29d9bd92c539e Mon Sep 17 00:00:00 2001 From: zhangtao Date: Tue, 27 Oct 2026 11:45:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=85=8D=E7=BD=AE=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E4=BC=98=E5=85=88=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 13 ++++++++++ .gitignore | 1 + README.md | 2 ++ README_EN.md | 2 ++ requirements.txt | 2 +- scripts/platform-db-setup.mjs | 2 ++ scripts/setup.mjs | 2 ++ src/config.js | 6 ++++- src/envFile.js | 45 +++++++++++++++++++++++++++++++++++ tests/configFile.test.js | 26 ++++++++++++++++++++ yolo-conf.yaml | 2 +- 11 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 src/envFile.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..636e33c --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Copy to .env for machine-local settings. .env is ignored by Git. +# Process environment variables override .env; .env overrides yolo-conf.yaml. + +PLATFORM_DB_BACKEND=mysql +PLATFORM_DB_HOST= +PLATFORM_DB_PORT=3306 +PLATFORM_DB_NAME= +PLATFORM_DB_USER= +PLATFORM_DB_PASSWORD= + +API_BASE_URL=https://open.bigmodel.cn/api/paas/v4 +API_KEY= +MODEL=GLM-4.7-Flash diff --git a/.gitignore b/.gitignore index 76411f7..caeae3b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data/* # kept ignored so a leftover file with real credentials can never be committed by accident. .env .env.* +!.env.example node_modules/ .venv/ diff --git a/README.md b/README.md index dba71ab..bc432d7 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,8 @@ docker compose up -d --build 服务启动后访问 [http://localhost:8088/](http://localhost:8088/)。按需在 `yolo-conf.yaml` 中配置模型和数据源;首次登录后请修改默认口令。详细配置见下方配置说明。 +本机账号、密码和 API Key 建议放在 `.env`(从 `.env.example` 复制),不要提交;优先级为进程环境变量 > `.env` > `yolo-conf.yaml`。 + PostgreSQL 数据源接入方式见 [PostgreSQL 接入文档](docs/postgresql.md)。 diff --git a/README_EN.md b/README_EN.md index 1d4511d..bfc821d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -153,6 +153,8 @@ docker compose up -d --build Open [http://localhost:8088/](http://localhost:8088/) after startup. Configure model and data-source settings in `yolo-conf.yaml` as needed; change the default password after your first login. See Configuration below for details. +Keep machine-local passwords and API keys in `.env` (copy `.env.example`); do not commit it. Precedence is process environment > `.env` > `yolo-conf.yaml`. + For PostgreSQL data sources, see the [PostgreSQL integration guide](docs/postgresql.md). | Username | Display name | Role | Default password | diff --git a/requirements.txt b/requirements.txt index aee8365..97a051a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ # Python analysis runtime required by execute_analysis_code and XLSX generation. -pandas==2.3.2 +pandas==2.3.2,<3 openpyxl>=3.1,<4 diff --git a/scripts/platform-db-setup.mjs b/scripts/platform-db-setup.mjs index 1c576f7..c406f42 100644 --- a/scripts/platform-db-setup.mjs +++ b/scripts/platform-db-setup.mjs @@ -17,9 +17,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parse } from 'yaml'; +import { loadEnvFile } from '../src/envFile.js'; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const confPath = path.resolve(projectRoot, 'yolo-conf.yaml'); +loadEnvFile({ projectRoot }); function readConf() { try { diff --git a/scripts/setup.mjs b/scripts/setup.mjs index fe908fe..9adbd10 100644 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -14,6 +14,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ensurePythonAnalysisRuntime } from './python-setup.mjs'; import { resolveConfPath } from './confFileEdit.mjs'; +import { loadEnvFile } from '../src/envFile.js'; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const MIN_NODE_MAJOR = 22; @@ -98,6 +99,7 @@ function setupPythonRuntime(confPath) { } console.log('\nYOLO Data 环境初始化\n'); +loadEnvFile({ projectRoot }); assertNodeVersion(); installDependencies(); const confPath = ensureConfigFile(); diff --git a/src/config.js b/src/config.js index 30b5776..8e369a3 100644 --- a/src/config.js +++ b/src/config.js @@ -4,6 +4,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { resolveAuthMode } from './auth.js'; import { applyConfigFile } from './configFile.js'; +import { loadEnvFile } from './envFile.js'; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); @@ -39,7 +40,10 @@ function parsePathList(value, fallback = []) { export function loadConfig(env = process.env) { // 配置面:yolo-conf.yaml 提供结构与默认值,真实环境变量覆盖;平台不读取 .env 文件。 // `env === process.env` 是「真实进程」守卫:测试传自定义 env 时不读工作区配置文件。 - if (env === process.env) applyConfigFile({ projectRoot }); + if (env === process.env) { + loadEnvFile({ projectRoot, target: env }); + applyConfigFile({ projectRoot, target: env }); + } const nodeEnv = String(env.NODE_ENV ?? '').trim().toLowerCase(); const authMode = resolveAuthMode({ authMode: env.AUTH_MODE ?? '', nodeEnv }); const host = String(env.HOST ?? '').trim() || (authMode === 'dev' ? '127.0.0.1' : '0.0.0.0'); diff --git a/src/envFile.js b/src/envFile.js new file mode 100644 index 0000000..1ca2013 --- /dev/null +++ b/src/envFile.js @@ -0,0 +1,45 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +function parseEnvValue(value) { + const trimmed = value.trim(); + const quote = trimmed[0]; + if ((quote === '"' || quote === "'" || quote === '`') && trimmed.endsWith(quote)) { + const body = trimmed.slice(1, -1); + return quote === '"' + ? body.replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\"/g, '"').replace(/\\\\/g, '\\') + : body; + } + return trimmed.replace(/\s+#.*$/, '').trim(); +} + +export function parseEnvFile(text) { + const values = {}; + for (const line of String(text ?? '').split(/\r?\n/)) { + const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/); + if (!match) continue; + values[match[1]] = parseEnvValue(match[2]); + } + return values; +} + +// Existing process environment always wins; .env fills only unset/empty keys. +export function loadEnvFile({ + projectRoot, + filePath = path.join(projectRoot, '.env'), + target = process.env, +} = {}) { + if (!fs.existsSync(filePath)) return { filePath, loaded: [], skipped: [] }; + const values = parseEnvFile(fs.readFileSync(filePath, 'utf8')); + const loaded = []; + const skipped = []; + for (const [key, value] of Object.entries(values)) { + if (target[key] === undefined || target[key] === '') { + target[key] = value; + loaded.push(key); + } else { + skipped.push(key); + } + } + return { filePath, loaded, skipped }; +} diff --git a/tests/configFile.test.js b/tests/configFile.test.js index fef03c5..e87ae55 100644 --- a/tests/configFile.test.js +++ b/tests/configFile.test.js @@ -5,6 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import YAML from 'yaml'; +import { loadEnvFile, parseEnvFile } from '../src/envFile.js'; import { CONFIG_FILE_MAP, applyConfigFile, @@ -78,6 +79,31 @@ models: assert.deepEqual(result.skipped, ['PORT', 'API_KEY']); }); +test('.env 解析:支持注释、export 和带特殊字符的引号值', () => { + assert.deepEqual(parseEnvFile(` +# comment +export API_KEY = "secret value # kept" +PLATFORM_DB_PASSWORD='p@ss word' +PORT=8088 # inline comment +`), { + API_KEY: 'secret value # kept', + PLATFORM_DB_PASSWORD: 'p@ss word', + PORT: '8088', + }); +}); + +test('.env 优先于 yolo-conf,但不覆盖进程环境变量', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'yolo-env-')); + const file = path.join(dir, '.env'); + fs.writeFileSync(file, 'PLATFORM_DB_HOST=from-env-file\nPLATFORM_DB_PASSWORD=local-secret\n', 'utf8'); + const target = { PLATFORM_DB_HOST: '', PLATFORM_DB_PASSWORD: 'process-secret' }; + const result = loadEnvFile({ projectRoot: dir, filePath: file, target }); + assert.equal(target.PLATFORM_DB_HOST, 'from-env-file'); + assert.equal(target.PLATFORM_DB_PASSWORD, 'process-secret'); + assert.deepEqual(result.loaded, ['PLATFORM_DB_HOST']); + assert.deepEqual(result.skipped, ['PLATFORM_DB_PASSWORD']); +}); + test('配置文件:文件缺失时安静返回', () => { const target = {}; const result = applyConfigFile({ diff --git a/yolo-conf.yaml b/yolo-conf.yaml index 8d3ca58..3354622 100644 --- a/yolo-conf.yaml +++ b/yolo-conf.yaml @@ -42,7 +42,7 @@ platformDatabase: port: 3306 database: yolo-data user: datagrouptest - password: "OUl2u82p83$#2" + password: "" charset: utf8mb4 initialize: true initSqlPath: ./config/platform/mysql-init.sql