feat: migrate runtime backend to node server

This commit is contained in:
victo
2026-04-08 16:41:29 +08:00
parent 9d2fc9e4b8
commit a83841ff2d
70 changed files with 8239 additions and 1561 deletions

162
server-node/src/config.ts Normal file
View File

@@ -0,0 +1,162 @@
import fs from 'node:fs';
import path from 'node:path';
export type AppConfig = {
nodeEnv: string;
projectRoot: string;
publicDir: string;
logsDir: string;
dataDir: string;
sqlitePath: string;
serverAddr: string;
logLevel: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
jwtSecret: string;
jwtExpiresIn: string;
jwtIssuer: string;
llm: {
baseUrl: string;
apiKey: string;
model: string;
};
dashScope: {
baseUrl: string;
apiKey: string;
imageModel: string;
requestTimeoutMs: number;
};
};
type LoadConfigOptions = {
env?: NodeJS.ProcessEnv;
projectRoot?: string;
};
function parseEnvContents(contents: string) {
return contents
.split(/\r?\n/u)
.reduce<Record<string, string>>((envMap, rawLine) => {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
return envMap;
}
const separatorIndex = line.indexOf('=');
if (separatorIndex < 0) {
return envMap;
}
const key = line.slice(0, separatorIndex).trim();
let value = line.slice(separatorIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
envMap[key] = value;
return envMap;
}, {});
}
function readEnvFile(filePath: string) {
if (!fs.existsSync(filePath)) {
return {};
}
return parseEnvContents(fs.readFileSync(filePath, 'utf8'));
}
function resolveDefaultProjectRoot() {
const cwd = process.cwd();
return path.basename(cwd) === 'server-node'
? path.resolve(cwd, '..')
: cwd;
}
function readMergedEnv(projectRoot: string, processEnv: NodeJS.ProcessEnv) {
return {
...readEnvFile(path.join(projectRoot, '.env.example')),
...readEnvFile(path.join(projectRoot, '.env.local')),
...processEnv,
};
}
function readString(
env: Record<string, string | undefined>,
key: string,
fallback: string,
) {
const value = env[key]?.trim();
return value ? value : fallback;
}
function readPositiveInt(
env: Record<string, string | undefined>,
key: string,
fallback: number,
) {
const parsed = Number(env[key]);
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : fallback;
}
export function loadConfig(options: LoadConfigOptions = {}): AppConfig {
const projectRoot = options.projectRoot ?? resolveDefaultProjectRoot();
const env = readMergedEnv(projectRoot, options.env ?? process.env);
const logsDir = path.join(projectRoot, 'server-node', 'logs');
const dataDir = path.join(projectRoot, 'server-node', 'data');
return {
nodeEnv: readString(env, 'NODE_ENV', 'development'),
projectRoot,
publicDir: path.join(projectRoot, 'public'),
logsDir,
dataDir,
sqlitePath: readString(
env,
'SQLITE_PATH',
path.join(dataDir, 'genarrative.sqlite'),
),
serverAddr: readString(env, 'NODE_SERVER_ADDR', ':8081'),
logLevel: readString(env, 'LOG_LEVEL', 'info') as AppConfig['logLevel'],
jwtSecret: readString(env, 'JWT_SECRET', 'genarrative-dev-secret'),
jwtExpiresIn: readString(env, 'JWT_EXPIRES_IN', '7d'),
jwtIssuer: readString(env, 'JWT_ISSUER', 'genarrative-server-node'),
llm: {
baseUrl: readString(
env,
'LLM_BASE_URL',
'https://ark.cn-beijing.volces.com/api/v3',
),
apiKey:
env.LLM_API_KEY?.trim() ||
env.ARK_API_KEY?.trim() ||
env.VITE_LLM_API_KEY?.trim() ||
'',
model: readString(
env,
'LLM_MODEL',
readString(
env,
'VITE_LLM_MODEL',
'doubao-1-5-pro-32k-character-250715',
),
),
},
dashScope: {
baseUrl: readString(
env,
'DASHSCOPE_BASE_URL',
'https://dashscope.aliyuncs.com/api/v1',
),
apiKey: env.DASHSCOPE_API_KEY?.trim() || '',
imageModel: readString(env, 'DASHSCOPE_IMAGE_MODEL', 'wan2.2-t2i-flash'),
requestTimeoutMs: readPositiveInt(
env,
'DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS',
150000,
),
},
};
}