Files
order_site/apps/backend/src/config/runtime.js
T

425 lines
12 KiB
JavaScript

// @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)
}