Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。


Expand Down
2 changes: 2 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions scripts/platform-db-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions scripts/setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -98,6 +99,7 @@ function setupPythonRuntime(confPath) {
}

console.log('\nYOLO Data 环境初始化\n');
loadEnvFile({ projectRoot });
assertNodeVersion();
installDependencies();
const confPath = ensureConfigFile();
Expand Down
6 changes: 5 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)), '..');

Expand Down Expand Up @@ -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');
Expand Down
45 changes: 45 additions & 0 deletions src/envFile.js
Original file line number Diff line number Diff line change
@@ -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 };
}
26 changes: 26 additions & 0 deletions tests/configFile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion yolo-conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading