// @ts-check import fs from 'node:fs' import path from 'node:path' import process from 'node:process' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' /** @typedef {import('../types/runtime-config.js').RuntimeConfig} RuntimeConfig */ const require = createRequire(import.meta.url) const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)) export const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') const CONFIG_ROOT = path.join(PROJECT_ROOT, 'config') loadEnvFiles([ path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env'), ]) const defaultConfig = /** @type {RuntimeConfig} */ (loadConfig(path.join(CONFIG_ROOT, 'default.cjs'))) export const runtimeConfig = /** @type {RuntimeConfig} */ (applyEnvOverrides(defaultConfig)) function loadConfig(configPath) { if (!fs.existsSync(configPath)) { return {} } const loaded = require(configPath) return isPlainObject(loaded) ? loaded : {} } function loadEnvFiles(filePaths) { for (const filePath of filePaths) { loadEnvFile(filePath) } } function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) { return } const rawText = fs.readFileSync(filePath, 'utf8') const lines = rawText.split(/\r?\n/) for (const rawLine of lines) { const line = rawLine.trim() if (!line || line.startsWith('#')) { continue } const separatorIndex = line.indexOf('=') if (separatorIndex <= 0) { continue } const key = line.slice(0, separatorIndex).trim() if (!key || key in process.env) { continue } process.env[key] = parseEnvValue(line.slice(separatorIndex + 1)) } } function parseEnvValue(rawValue) { const value = String(rawValue || '').trim() if (!value) { return '' } const quote = value[0] if ((quote === '"' || quote === "'") && value.endsWith(quote)) { return value.slice(1, -1) } return value } function applyEnvOverrides(baseConfig) { const nextConfig = deepMerge(baseConfig, {}) const port = parseInteger(process.env.PORT) if (port !== null) { nextConfig.server.port = port } const chromePath = String(process.env.CHROME_PATH || '').trim() if (chromePath) { nextConfig.browser.chromePath = chromePath } const browserHeadless = parseBoolean(process.env.TENCENT_BROWSER_HEADLESS) if (browserHeadless !== null) { nextConfig.browser.headless = browserHeadless } const browserDevtools = parseBoolean(process.env.TENCENT_BROWSER_DEVTOOLS) if (browserDevtools !== null) { nextConfig.browser.devtools = browserDevtools } const browserKeepAlive = parseBoolean(process.env.TENCENT_BROWSER_KEEP_ALIVE) if (browserKeepAlive !== null) { nextConfig.browser.keepAlive = browserKeepAlive } const browserPrewarm = parseBoolean(process.env.TENCENT_BROWSER_PREWARM) if (browserPrewarm !== null) { nextConfig.browser.prewarm = browserPrewarm } const browserSlowMoMs = parseInteger(process.env.TENCENT_BROWSER_SLOW_MO) if (browserSlowMoMs !== null) { nextConfig.browser.slowMoMs = browserSlowMoMs } const sessionDebug = parseBoolean(process.env.TENCENT_SESSION_DEBUG) if (sessionDebug !== null) { nextConfig.session.debug = sessionDebug } const ocrProjectRoot = String(process.env.OCR_PROJECT_ROOT || '').trim() if (ocrProjectRoot) { nextConfig.ocr.projectRoot = ocrProjectRoot } const dataRoot = String(process.env.DATA_ROOT || '').trim() if (dataRoot) { nextConfig.data.root = path.resolve(dataRoot) } const logLevel = String(process.env.LOG_LEVEL || '').trim() if (logLevel) { nextConfig.logging.level = logLevel } const databaseUrl = String(process.env.DATABASE_URL || '').trim() if (databaseUrl) { nextConfig.database.url = databaseUrl } const databaseSsl = parseBoolean(process.env.DATABASE_SSL) if (databaseSsl !== null) { nextConfig.database.ssl = databaseSsl } const databaseMaxConnections = parseInteger(process.env.DATABASE_MAX_CONNECTIONS) if (databaseMaxConnections !== null) { nextConfig.database.maxConnections = databaseMaxConnections } const claimBaseUrl = String(process.env.CLAIM_BASE_URL || '').trim() if (claimBaseUrl) { nextConfig.orders.claimBaseUrl = claimBaseUrl } else { const appBaseUrl = String(process.env.APP_BASE_URL || '').trim() if (appBaseUrl) { nextConfig.orders.claimBaseUrl = `${appBaseUrl.replace(/\/+$/, '')}/#/claim` } } const tokenTtlHours = parseInteger(process.env.CLAIM_TOKEN_TTL_HOURS) if (tokenTtlHours !== null) { nextConfig.orders.tokenTtlHours = tokenTtlHours } const adminSessionSecret = String(process.env.ADMIN_SESSION_SECRET || '').trim() if (adminSessionSecret) { nextConfig.admin.sessionSecret = adminSessionSecret } const adminSessionTtlHours = parseInteger(process.env.ADMIN_SESSION_TTL_HOURS) if (adminSessionTtlHours !== null) { nextConfig.admin.sessionTtlHours = adminSessionTtlHours } const adminDefaultUsers = parseJsonArray(process.env.ADMIN_DEFAULT_USERS_JSON) if (adminDefaultUsers) { nextConfig.admin.defaultUsers = adminDefaultUsers } const agisoAppSecret = String(process.env.AGISO_APP_SECRET || '').trim() if (agisoAppSecret) { nextConfig.platforms.agiso.appSecret = agisoAppSecret } const agisoTradeDetailEndpoint = String(process.env.AGISO_TRADE_DETAIL_ENDPOINT || '').trim() if (agisoTradeDetailEndpoint) { nextConfig.platforms.agiso.tradeDetail.endpoint = agisoTradeDetailEndpoint } const agisoTradeDetailApiVersion = String(process.env.AGISO_TRADE_DETAIL_API_VERSION || '').trim() if (agisoTradeDetailApiVersion) { nextConfig.platforms.agiso.tradeDetail.apiVersion = agisoTradeDetailApiVersion } const agisoTradeDetailTimeoutMs = parseInteger(process.env.AGISO_TRADE_DETAIL_TIMEOUT_MS) if (agisoTradeDetailTimeoutMs !== null) { nextConfig.platforms.agiso.tradeDetail.timeoutMs = agisoTradeDetailTimeoutMs } const agisoAutoDeliveryEnabled = parseBoolean(process.env.AGISO_AUTO_DELIVERY_ENABLED) if (agisoAutoDeliveryEnabled !== null) { nextConfig.platforms.agiso.autoDelivery.enabled = agisoAutoDeliveryEnabled } const agisoAutoDeliveryEndpoint = String(process.env.AGISO_AUTO_DELIVERY_ENDPOINT || '').trim() if (agisoAutoDeliveryEndpoint) { nextConfig.platforms.agiso.autoDelivery.endpoint = agisoAutoDeliveryEndpoint } const agisoAutoDeliveryApiVersion = String(process.env.AGISO_AUTO_DELIVERY_API_VERSION || '').trim() if (agisoAutoDeliveryApiVersion) { nextConfig.platforms.agiso.autoDelivery.apiVersion = agisoAutoDeliveryApiVersion } const agisoMessageApiVersion = String(process.env.AGISO_MESSAGE_API_VERSION || '').trim() if (agisoMessageApiVersion) { nextConfig.platforms.agiso.messaging.apiVersion = agisoMessageApiVersion } const agisoSendMessageEndpoint = String(process.env.AGISO_SEND_MESSAGE_ENDPOINT || '').trim() if (agisoSendMessageEndpoint) { nextConfig.platforms.agiso.messaging.sendMessageEndpoint = agisoSendMessageEndpoint } const agisoMessagingEnabled = parseBoolean(process.env.AGISO_MESSAGING_ENABLED) if (agisoMessagingEnabled !== null) { nextConfig.platforms.agiso.messaging.enabled = agisoMessagingEnabled } const khhaoBaseUrl = String(process.env.KHHAO_BASE_URL || '').trim() if (khhaoBaseUrl) { nextConfig.platforms.khhao.baseUrl = khhaoBaseUrl } const khhaoTimeoutMs = parseInteger(process.env.KHHAO_TIMEOUT_MS) if (khhaoTimeoutMs !== null) { nextConfig.platforms.khhao.timeoutMs = khhaoTimeoutMs } const khhaoLoginPath = String(process.env.KHHAO_LOGIN_PATH || '').trim() if (khhaoLoginPath) { nextConfig.platforms.khhao.loginPath = khhaoLoginPath } const khhaoCaptchaPath = String(process.env.KHHAO_CAPTCHA_PATH || '').trim() if (khhaoCaptchaPath) { nextConfig.platforms.khhao.captchaPath = khhaoCaptchaPath } const khhaoOrderListPath = String(process.env.KHHAO_ORDER_LIST_PATH || '').trim() if (khhaoOrderListPath) { nextConfig.platforms.khhao.orderListPath = khhaoOrderListPath } const cloudtentaclesBaseUrl = String(process.env.CLOUDTENTACLES_BASE_URL || '').trim() if (cloudtentaclesBaseUrl) { nextConfig.platforms.cloudtentacles.baseUrl = cloudtentaclesBaseUrl } const cloudtentaclesTimeoutMs = parseInteger(process.env.CLOUDTENTACLES_TIMEOUT_MS) if (cloudtentaclesTimeoutMs !== null) { nextConfig.platforms.cloudtentacles.timeoutMs = cloudtentaclesTimeoutMs } const cloudtentaclesSendSmsPath = String(process.env.CLOUDTENTACLES_SEND_SMS_PATH || '').trim() if (cloudtentaclesSendSmsPath) { nextConfig.platforms.cloudtentacles.sendSmsPath = cloudtentaclesSendSmsPath } const cloudtentaclesLoginPath = String(process.env.CLOUDTENTACLES_LOGIN_PATH || '').trim() if (cloudtentaclesLoginPath) { nextConfig.platforms.cloudtentacles.loginPath = cloudtentaclesLoginPath } const cloudtentaclesUserInfoPath = String(process.env.CLOUDTENTACLES_USER_INFO_PATH || '').trim() if (cloudtentaclesUserInfoPath) { nextConfig.platforms.cloudtentacles.userInfoPath = cloudtentaclesUserInfoPath } const cloudtentaclesAssetPath = String(process.env.CLOUDTENTACLES_ASSET_PATH || '').trim() if (cloudtentaclesAssetPath) { nextConfig.platforms.cloudtentacles.assetPath = cloudtentaclesAssetPath } const cloudtentaclesPermissionPath = String(process.env.CLOUDTENTACLES_PERMISSION_PATH || '').trim() if (cloudtentaclesPermissionPath) { nextConfig.platforms.cloudtentacles.permissionPath = cloudtentaclesPermissionPath } const cloudtentaclesCategoriesPath = String(process.env.CLOUDTENTACLES_CATEGORIES_PATH || '').trim() if (cloudtentaclesCategoriesPath) { nextConfig.platforms.cloudtentacles.categoriesPath = cloudtentaclesCategoriesPath } const cloudtentaclesSkuListPath = String(process.env.CLOUDTENTACLES_SKU_LIST_PATH || '').trim() if (cloudtentaclesSkuListPath) { nextConfig.platforms.cloudtentacles.skuListPath = cloudtentaclesSkuListPath } const cloudtentaclesSkuBuyPath = String(process.env.CLOUDTENTACLES_SKU_BUY_PATH || '').trim() if (cloudtentaclesSkuBuyPath) { nextConfig.platforms.cloudtentacles.skuBuyPath = cloudtentaclesSkuBuyPath } const cloudtentaclesKnapsackPath = String(process.env.CLOUDTENTACLES_KNAPSACK_PATH || '').trim() if (cloudtentaclesKnapsackPath) { nextConfig.platforms.cloudtentacles.knapsackPath = cloudtentaclesKnapsackPath } const cloudtentaclesVnListPath = String(process.env.CLOUDTENTACLES_VN_LIST_PATH || '').trim() if (cloudtentaclesVnListPath) { nextConfig.platforms.cloudtentacles.vnListPath = cloudtentaclesVnListPath } const cloudtentaclesVnAppointPath = String(process.env.CLOUDTENTACLES_VN_APPOINT_PATH || '').trim() if (cloudtentaclesVnAppointPath) { nextConfig.platforms.cloudtentacles.vnAppointPath = cloudtentaclesVnAppointPath } const cloudtentaclesVnGenerateLoginCodePath = String(process.env.CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH || '').trim() if (cloudtentaclesVnGenerateLoginCodePath) { nextConfig.platforms.cloudtentacles.vnGenerateLoginCodePath = cloudtentaclesVnGenerateLoginCodePath } const cloudtentaclesVnVerifCodePath = String(process.env.CLOUDTENTACLES_VN_VERIF_CODE_PATH || '').trim() if (cloudtentaclesVnVerifCodePath) { nextConfig.platforms.cloudtentacles.vnVerifCodePath = cloudtentaclesVnVerifCodePath } const cloudtentaclesVnVerifyLoginCodePath = String(process.env.CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH || '').trim() if (cloudtentaclesVnVerifyLoginCodePath) { nextConfig.platforms.cloudtentacles.vnVerifyLoginCodePath = cloudtentaclesVnVerifyLoginCodePath } const cloudtentaclesVnBindUrlPath = String(process.env.CLOUDTENTACLES_VN_BIND_URL_PATH || '').trim() if (cloudtentaclesVnBindUrlPath) { nextConfig.platforms.cloudtentacles.vnBindUrlPath = cloudtentaclesVnBindUrlPath } const cloudtentaclesPublicKeyPem = String(process.env.CLOUDTENTACLES_PUBLIC_KEY_PEM || '').trim() if (cloudtentaclesPublicKeyPem) { nextConfig.platforms.cloudtentacles.publicKeyPem = cloudtentaclesPublicKeyPem } const cloudtentaclesClientSource = String(process.env.CLOUDTENTACLES_CLIENT_SOURCE || '').trim() if (cloudtentaclesClientSource) { nextConfig.platforms.cloudtentacles.clientSource = cloudtentaclesClientSource } const cloudtentaclesDeviceId = String(process.env.CLOUDTENTACLES_DEVICE_ID || '').trim() if (cloudtentaclesDeviceId) { nextConfig.platforms.cloudtentacles.deviceId = cloudtentaclesDeviceId } const cloudtentaclesDeviceType = parseInteger(process.env.CLOUDTENTACLES_DEVICE_TYPE) if (cloudtentaclesDeviceType !== null) { nextConfig.platforms.cloudtentacles.deviceType = cloudtentaclesDeviceType } const proofMode = String(process.env.TENCENT_REDEEM_PROOF_MODE || '').trim() if (proofMode) { nextConfig.redeem.proofMode = proofMode } return nextConfig } function deepMerge(baseValue, overrideValue) { if (!isPlainObject(baseValue)) { return cloneValue(overrideValue) } const result = cloneValue(baseValue) if (!isPlainObject(overrideValue)) { return result } for (const [key, value] of Object.entries(overrideValue)) { if (isPlainObject(value) && isPlainObject(result[key])) { result[key] = deepMerge(result[key], value) continue } result[key] = cloneValue(value) } return result } function cloneValue(value) { if (Array.isArray(value)) { return value.map((item) => cloneValue(item)) } if (isPlainObject(value)) { const output = {} for (const [key, item] of Object.entries(value)) { output[key] = cloneValue(item) } return output } return value } function isPlainObject(value) { return Object.prototype.toString.call(value) === '[object Object]' } function parseBoolean(rawValue) { const normalized = String(rawValue || '').trim().toLowerCase() if (!normalized) { return null } if (['1', 'true', 'yes', 'on'].includes(normalized)) { return true } if (['0', 'false', 'no', 'off'].includes(normalized)) { return false } return null } function parseInteger(rawValue) { const normalized = String(rawValue || '').trim() if (!normalized) { return null } const parsed = Number(normalized) return Number.isFinite(parsed) ? parsed : null } function parseJsonArray(rawValue) { const normalized = String(rawValue || '').trim() if (!normalized) { return null } try { const parsed = JSON.parse(normalized) return Array.isArray(parsed) ? parsed : null } catch { return null } } function normalizeBooleanLike(value) { if (typeof value === 'boolean') { return value } return parseBoolean(value) }