// @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').AgisoMessagingShopsConfig} AgisoMessagingShopsConfig */ /** @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 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 agisoAutoDeliveryAldsType = parseInteger(process.env.AGISO_AUTO_DELIVERY_ALDS_TYPE) if (agisoAutoDeliveryAldsType !== null) { nextConfig.platforms.agiso.autoDelivery.aldsType = agisoAutoDeliveryAldsType } for (const [envKey, configKey] of [ ['AGISO_AUTO_DELIVERY_IGNORE_ALDS_LOG', 'ignoreAldsLog'], ['AGISO_AUTO_DELIVERY_IGNORE_BLACK_LIST', 'ignoreBlackList'], ['AGISO_AUTO_DELIVERY_IGNORE_ON_OFF', 'ignoreOnOff'], ['AGISO_AUTO_DELIVERY_IGNORE_REFUND_CHECK', 'ignoreRefundCheck'], ['AGISO_AUTO_DELIVERY_IGNORE_RESTRICTED', 'ignoreRestricted'], ['AGISO_AUTO_DELIVERY_IGNORE_TRADE_STATUS_CHECK', 'ignoreTradeStatusCheck'], ]) { const parsed = parseBoolean(process.env[envKey]) if (parsed !== null) { nextConfig.platforms.agiso.autoDelivery[configKey] = parsed } } const agisoAppId = String(process.env.AGISO_APP_ID || '').trim() if (agisoAppId) { nextConfig.platforms.agiso.messaging.appId = agisoAppId } const agisoAccessToken = String(process.env.AGISO_ACCESS_TOKEN || '').trim() if (agisoAccessToken) { nextConfig.platforms.agiso.messaging.accessToken = agisoAccessToken } const agisoMessageAppSecret = String(process.env.AGISO_MESSAGE_APP_SECRET || '').trim() if (agisoMessageAppSecret) { nextConfig.platforms.agiso.messaging.appSecret = agisoMessageAppSecret } 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 agisoMessageTemplate = String(process.env.AGISO_MESSAGE_TEMPLATE || '').trim() if (agisoMessageTemplate) { nextConfig.platforms.agiso.messaging.messageTemplate = agisoMessageTemplate } const agisoShops = parseJsonObject(process.env.AGISO_SHOPS_JSON) if (agisoShops) { nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops) } 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 parseJsonObject(rawValue) { const normalized = String(rawValue || '').trim() if (!normalized) { return null } try { const parsed = JSON.parse(normalized) return isPlainObject(parsed) ? parsed : null } catch { return 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 normalizeAgisoMessagingShops(rawValue) { /** @type {AgisoMessagingShopsConfig} */ const output = {} for (const [shopId, config] of Object.entries(rawValue || {})) { const normalizedShopId = String(shopId || '').trim() if (!normalizedShopId || !isPlainObject(config)) { continue } /** @type {AgisoMessagingShopsConfig[string]} */ const next = {} const enabled = normalizeBooleanLike(config.enabled) if (enabled !== null) { next.enabled = enabled } for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint']) { const value = String(config[key] || '').trim() if (value) { next[key] = value } } output[normalizedShopId] = next } return output } function normalizeBooleanLike(value) { if (typeof value === 'boolean') { return value } return parseBoolean(value) }