优化配置文件
This commit is contained in:
+6
-5
@@ -9,8 +9,8 @@ CLAIM_TOKEN_TTL_HOURS=24
|
|||||||
|
|
||||||
POSTGRES_DB=order_site
|
POSTGRES_DB=order_site
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=FACCqheAzy51op0RFdbv
|
POSTGRES_PASSWORD=change-me-postgres-password
|
||||||
DATABASE_URL=postgres://postgres:FACCqheAzy51op0RFdbv@postgres:5432/order_site
|
DATABASE_URL=postgres://postgres:change-me-postgres-password@postgres:5432/order_site
|
||||||
DATABASE_SSL=false
|
DATABASE_SSL=false
|
||||||
DATABASE_MAX_CONNECTIONS=20
|
DATABASE_MAX_CONNECTIONS=20
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ TENCENT_BROWSER_DEVTOOLS=false
|
|||||||
TENCENT_BROWSER_PREWARM=true
|
TENCENT_BROWSER_PREWARM=true
|
||||||
TENCENT_BROWSER_KEEP_ALIVE=true
|
TENCENT_BROWSER_KEEP_ALIVE=true
|
||||||
TENCENT_BROWSER_SLOW_MO=0
|
TENCENT_BROWSER_SLOW_MO=0
|
||||||
|
TENCENT_BROWSER_VNC_RESOLUTION=1600x900x24
|
||||||
TENCENT_SESSION_DEBUG=false
|
TENCENT_SESSION_DEBUG=false
|
||||||
CLOUDTENTACLES_BIND_URL_TTL_SECONDS=600
|
CLOUDTENTACLES_BIND_URL_TTL_SECONDS=600
|
||||||
CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS=30
|
CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS=30
|
||||||
@@ -35,11 +36,11 @@ CLOUDTENTACLES_BIND_URL_PROBE_IDE_TOKEN=z90Syo
|
|||||||
CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL=http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F
|
CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL=http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F
|
||||||
CLOUDTENTACLES_BIND_URL_PROBE_REFERER=https://gp.qq.com/
|
CLOUDTENTACLES_BIND_URL_PROBE_REFERER=https://gp.qq.com/
|
||||||
|
|
||||||
ADMIN_SESSION_SECRET=xm4BDpbdHJvU4RIlpGqD0nvQTZtIidvQMk6LSP1d
|
ADMIN_SESSION_SECRET=change-me-long-random-session-secret
|
||||||
ADMIN_SESSION_TTL_HOURS=12
|
ADMIN_SESSION_TTL_HOURS=12
|
||||||
ADMIN_DEFAULT_USERS_JSON=[{"username":"admin","password":"Mzl5iG6pkdrzLs73COiB","role":"admin"},{"username":"operator","password":"d9tvmbPPMRdLySEhORzk","role":"operator"}]
|
ADMIN_DEFAULT_USERS_JSON=[{"username":"admin","password":"change-me-admin-password","role":"admin"},{"username":"operator","password":"change-me-operator-password","role":"operator"}]
|
||||||
|
|
||||||
AGISO_APP_SECRET=tccxk5c7ppy7xpr43rvastceyydskfha
|
AGISO_APP_SECRET=change-me-agiso-app-secret
|
||||||
AGISO_MESSAGING_ENABLED=true
|
AGISO_MESSAGING_ENABLED=true
|
||||||
AGISO_TRADE_DETAIL_ENDPOINT=https://gw-api.agiso.com/aldsIdle/Order/Detail
|
AGISO_TRADE_DETAIL_ENDPOINT=https://gw-api.agiso.com/aldsIdle/Order/Detail
|
||||||
AGISO_TRADE_DETAIL_API_VERSION=1
|
AGISO_TRADE_DETAIL_API_VERSION=1
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ npm test
|
|||||||
|
|
||||||
当前约定:
|
当前约定:
|
||||||
|
|
||||||
- 开发:`npm run dev` 使用 `tsx watch` 直接运行 `src`,并额外监听 `config`
|
- 开发:`npm run dev` 使用 `tsx watch` 直接运行 `src`
|
||||||
- 数据库迁移:`npm run db:migrate` 使用 `tsx`
|
- 数据库迁移:`npm run db:migrate` 使用 `tsx`
|
||||||
- 生产:`npm run build` 输出 `dist`,`npm run start` 运行 `dist/index.js`
|
- 生产:`npm run build` 输出 `dist`,`npm run start` 运行 `dist/index.js`
|
||||||
- 新增或迁移后端模块优先使用 `.ts`
|
- 新增或迁移后端模块优先使用 `.ts`
|
||||||
@@ -53,7 +53,7 @@ npm test
|
|||||||
|
|
||||||
当前后端采用 `.env` 单一路径做环境配置:
|
当前后端采用 `.env` 单一路径做环境配置:
|
||||||
|
|
||||||
- 通用默认值:`config/default.cjs`
|
- 通用默认值:`src/config/defaults.ts`
|
||||||
- 本地/部署覆盖:工作区根目录 `.env`
|
- 本地/部署覆盖:工作区根目录 `.env`
|
||||||
|
|
||||||
当前最常改的配置有:
|
当前最常改的配置有:
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
const path = require('node:path')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
},
|
|
||||||
|
|
||||||
browser: {
|
|
||||||
chromePath: '',
|
|
||||||
headless: null,
|
|
||||||
devtools: null,
|
|
||||||
keepAlive: null,
|
|
||||||
prewarm: null,
|
|
||||||
slowMoMs: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
session: {
|
|
||||||
debug: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
ocr: {
|
|
||||||
baseUrl: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
data: {
|
|
||||||
root: path.resolve(__dirname, '../data'),
|
|
||||||
},
|
|
||||||
|
|
||||||
logging: {
|
|
||||||
level: 'info',
|
|
||||||
},
|
|
||||||
|
|
||||||
database: {
|
|
||||||
url: 'postgres://postgres:postgres@127.0.0.1:5432/order_site',
|
|
||||||
ssl: false,
|
|
||||||
maxConnections: 10,
|
|
||||||
},
|
|
||||||
|
|
||||||
orders: {
|
|
||||||
claimBaseUrl: 'http://127.0.0.1:5173/#/claim',
|
|
||||||
tokenTtlHours: 24,
|
|
||||||
},
|
|
||||||
|
|
||||||
admin: {
|
|
||||||
sessionSecret: '',
|
|
||||||
sessionTtlHours: 12,
|
|
||||||
defaultUsers: [],
|
|
||||||
},
|
|
||||||
|
|
||||||
platforms: {
|
|
||||||
agiso: {
|
|
||||||
appSecret: '',
|
|
||||||
tradeDetail: {
|
|
||||||
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/Detail',
|
|
||||||
apiVersion: '1',
|
|
||||||
timeoutMs: 5000,
|
|
||||||
},
|
|
||||||
autoDelivery: {
|
|
||||||
enabled: true,
|
|
||||||
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/DummySend',
|
|
||||||
apiVersion: '1',
|
|
||||||
},
|
|
||||||
messaging: {
|
|
||||||
enabled: false,
|
|
||||||
sendMessageEndpoint: 'https://gw-api.agiso.com/aldsIdle/ImMsg/SendMsg',
|
|
||||||
apiVersion: '1',
|
|
||||||
messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}',
|
|
||||||
autoDeliveryMessageTemplate: '您的订单 {platformOrderId} 已完成自动发货,请注意查收。',
|
|
||||||
shops: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ninetyone: {
|
|
||||||
userId: '',
|
|
||||||
secret: '',
|
|
||||||
version: '1.0',
|
|
||||||
shopId: '91kaquan',
|
|
||||||
shopName: '91卡券',
|
|
||||||
timestampToleranceSeconds: 600,
|
|
||||||
cardsEncoding: 'aes-256-ecb-base64',
|
|
||||||
},
|
|
||||||
cloudtentacles: {
|
|
||||||
baseUrl: 'https://123.207.217.176',
|
|
||||||
timeoutMs: 5000,
|
|
||||||
sendSmsPath: '/public/verif_code',
|
|
||||||
loginPath: '/public/login',
|
|
||||||
userInfoPath: '/user/info',
|
|
||||||
assetPath: '/user/get_asset',
|
|
||||||
permissionPath: '/user/get_permission',
|
|
||||||
categoriesPath: '/categories/get',
|
|
||||||
skuListPath: '/sku/list',
|
|
||||||
skuBuyPath: '/sku/buy',
|
|
||||||
skuUsePath: '/sku/use',
|
|
||||||
knapsackPath: '/user/get_knapsack',
|
|
||||||
vnListPath: '/vn/list',
|
|
||||||
vnAppointPath: '/vn/appoint',
|
|
||||||
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
|
||||||
vnVerifCodePath: '/public/vn_verif_code',
|
|
||||||
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
|
||||||
vnBindUrlPath: '/vn/bind_url',
|
|
||||||
vnBindInfoPath: '/vn/bind_info',
|
|
||||||
vnBackPath: '/vn/back',
|
|
||||||
bindUrlTtlSeconds: 600,
|
|
||||||
bindUrlProbeIntervalSeconds: 30,
|
|
||||||
bindUrlProbeTimeoutMs: 5000,
|
|
||||||
bindUrlProbeUserAgent: 'Mozilla/5.0 (Linux; Android 12; M2012K11AC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/98.0.4758.102 MQQBrowser/6.2 TBS/046141 Mobile Safari/537.36 V1_AND_SQ_8.8.85_2674_YYB_D QQ/8.8.85.7685 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/108 SimpleUISwitch/0 QQTheme/1000',
|
|
||||||
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
|
||||||
bindUrlProbeChartId: '323794',
|
|
||||||
bindUrlProbeSubChartId: '323794',
|
|
||||||
bindUrlProbeIdeToken: 'z90Syo',
|
|
||||||
bindUrlProbeActivityUrl: 'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F',
|
|
||||||
bindUrlProbeReferer: 'https://gp.qq.com/',
|
|
||||||
bindUrlProbeExtraCookie: '',
|
|
||||||
clientSource: 'ct-client',
|
|
||||||
deviceId: '-',
|
|
||||||
deviceType: 0,
|
|
||||||
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsSzjk+9st2f8IpYqYaM5
|
|
||||||
F7K27ZR2IvzxTr2w4zgZAS2+BRdgMvXUOITdFQT+ytYF/r3pIgTkL7MF9UQuylmb
|
|
||||||
S57DiqGBLNBS7FzKyYzWTOc9yYA3+KrarbvpUn/Bm3UK+KKB17viPy+0pnNAHysJ
|
|
||||||
pzqadmAtQhI1ZDujJ2sW+FkV/pC8KIFahiyPIHjncy1ZG/BbRF3BDhvDcr6O7scO
|
|
||||||
GsHstGf9XsZBIITS5LYmlai7OdY4ZIVxUtQbfWzibihO+YDJJzFgt47drX0Ajo85
|
|
||||||
PfQTPNA++KsXtwRX9M6Re3vkTDRsutIIWKtj8jqhUbbYS3vzS8GJnAWavUFVkR15
|
|
||||||
0wIDAQAB
|
|
||||||
-----END PUBLIC KEY-----`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
redeem: {
|
|
||||||
proofMode: 'full',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"seed:dev-data": "tsx scripts/seed-dev-data.ts",
|
"seed:dev-data": "tsx scripts/seed-dev-data.ts",
|
||||||
"build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
"build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
||||||
"db:migrate": "tsx src/db/migrate.ts",
|
"db:migrate": "tsx src/db/migrate.ts",
|
||||||
"dev": "tsx watch --clear-screen=false --include config src/index.ts",
|
"dev": "tsx watch --clear-screen=false src/index.ts",
|
||||||
"test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)",
|
"test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import type { RuntimeConfig } from '../types/runtime-config.js'
|
||||||
|
|
||||||
|
export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
|
||||||
|
browser: {
|
||||||
|
chromePath: '',
|
||||||
|
headless: null,
|
||||||
|
devtools: null,
|
||||||
|
keepAlive: null,
|
||||||
|
prewarm: null,
|
||||||
|
slowMoMs: null,
|
||||||
|
vncResolution: '1600x900x24',
|
||||||
|
},
|
||||||
|
|
||||||
|
session: {
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
ocr: {
|
||||||
|
baseUrl: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
data: {
|
||||||
|
root: path.resolve(projectRoot, 'data'),
|
||||||
|
},
|
||||||
|
|
||||||
|
logging: {
|
||||||
|
level: 'info',
|
||||||
|
},
|
||||||
|
|
||||||
|
database: {
|
||||||
|
url: 'postgres://postgres:postgres@127.0.0.1:5432/order_site',
|
||||||
|
ssl: false,
|
||||||
|
maxConnections: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
orders: {
|
||||||
|
claimBaseUrl: 'http://127.0.0.1:5173/#/claim',
|
||||||
|
tokenTtlHours: 24,
|
||||||
|
},
|
||||||
|
|
||||||
|
admin: {
|
||||||
|
sessionSecret: '',
|
||||||
|
sessionTtlHours: 12,
|
||||||
|
defaultUsers: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
platforms: {
|
||||||
|
agiso: {
|
||||||
|
appSecret: '',
|
||||||
|
tradeDetail: {
|
||||||
|
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/Detail',
|
||||||
|
apiVersion: '1',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
},
|
||||||
|
autoDelivery: {
|
||||||
|
enabled: true,
|
||||||
|
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/DummySend',
|
||||||
|
apiVersion: '1',
|
||||||
|
},
|
||||||
|
messaging: {
|
||||||
|
enabled: false,
|
||||||
|
sendMessageEndpoint: 'https://gw-api.agiso.com/aldsIdle/ImMsg/SendMsg',
|
||||||
|
apiVersion: '1',
|
||||||
|
messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}',
|
||||||
|
autoDeliveryMessageTemplate: '您的订单 {platformOrderId} 已完成自动发货,请注意查收。',
|
||||||
|
shops: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ninetyone: {
|
||||||
|
userId: '',
|
||||||
|
secret: '',
|
||||||
|
version: '1.0',
|
||||||
|
shopId: '91kaquan',
|
||||||
|
shopName: '91卡券',
|
||||||
|
timestampToleranceSeconds: 600,
|
||||||
|
cardsEncoding: 'aes-256-ecb-base64',
|
||||||
|
},
|
||||||
|
cloudtentacles: {
|
||||||
|
baseUrl: 'https://123.207.217.176',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
sendSmsPath: '/public/verif_code',
|
||||||
|
loginPath: '/public/login',
|
||||||
|
userInfoPath: '/user/info',
|
||||||
|
assetPath: '/user/get_asset',
|
||||||
|
permissionPath: '/user/get_permission',
|
||||||
|
categoriesPath: '/categories/get',
|
||||||
|
skuListPath: '/sku/list',
|
||||||
|
skuBuyPath: '/sku/buy',
|
||||||
|
skuUsePath: '/sku/use',
|
||||||
|
knapsackPath: '/user/get_knapsack',
|
||||||
|
vnListPath: '/vn/list',
|
||||||
|
vnAppointPath: '/vn/appoint',
|
||||||
|
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
||||||
|
vnVerifCodePath: '/public/vn_verif_code',
|
||||||
|
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||||
|
vnBindUrlPath: '/vn/bind_url',
|
||||||
|
vnBindInfoPath: '/vn/bind_info',
|
||||||
|
vnBackPath: '/vn/back',
|
||||||
|
bindUrlTtlSeconds: 600,
|
||||||
|
bindUrlProbeIntervalSeconds: 30,
|
||||||
|
bindUrlProbeTimeoutMs: 5000,
|
||||||
|
bindUrlProbeUserAgent: 'Mozilla/5.0 (Linux; Android 12; M2012K11AC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/98.0.4758.102 MQQBrowser/6.2 TBS/046141 Mobile Safari/537.36 V1_AND_SQ_8.8.85_2674_YYB_D QQ/8.8.85.7685 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/108 SimpleUISwitch/0 QQTheme/1000',
|
||||||
|
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
||||||
|
bindUrlProbeChartId: '323794',
|
||||||
|
bindUrlProbeSubChartId: '323794',
|
||||||
|
bindUrlProbeIdeToken: 'z90Syo',
|
||||||
|
bindUrlProbeActivityUrl: 'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F',
|
||||||
|
bindUrlProbeReferer: 'https://gp.qq.com/',
|
||||||
|
bindUrlProbeExtraCookie: '',
|
||||||
|
clientSource: 'ct-client',
|
||||||
|
deviceId: '-',
|
||||||
|
deviceType: 0,
|
||||||
|
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsSzjk+9st2f8IpYqYaM5
|
||||||
|
F7K27ZR2IvzxTr2w4zgZAS2+BRdgMvXUOITdFQT+ytYF/r3pIgTkL7MF9UQuylmb
|
||||||
|
S57DiqGBLNBS7FzKyYzWTOc9yYA3+KrarbvpUn/Bm3UK+KKB17viPy+0pnNAHysJ
|
||||||
|
pzqadmAtQhI1ZDujJ2sW+FkV/pC8KIFahiyPIHjncy1ZG/BbRF3BDhvDcr6O7scO
|
||||||
|
GsHstGf9XsZBIITS5LYmlai7OdY4ZIVxUtQbfWzibihO+YDJJzFgt47drX0Ajo85
|
||||||
|
PfQTPNA++KsXtwRX9M6Re3vkTDRsutIIWKtj8jqhUbbYS3vzS8GJnAWavUFVkR15
|
||||||
|
0wIDAQAB
|
||||||
|
-----END PUBLIC KEY-----`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
redeem: {
|
||||||
|
proofMode: 'full',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import { ENV_OVERRIDES } from './env-overrides.js'
|
||||||
|
|
||||||
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const BACKEND_ROOT = path.resolve(CURRENT_DIR, '../..')
|
||||||
|
const WORKSPACE_ROOT = path.resolve(BACKEND_ROOT, '../..')
|
||||||
|
|
||||||
|
const SPECIAL_RUNTIME_ENV_NAMES = [
|
||||||
|
'APP_BASE_URL',
|
||||||
|
'CLAIM_BASE_URL',
|
||||||
|
]
|
||||||
|
|
||||||
|
const DEPLOYMENT_ONLY_ENV_NAMES = [
|
||||||
|
'APP_DOMAIN',
|
||||||
|
'BACKEND_PORT',
|
||||||
|
'CADDY_SITE_ADDR',
|
||||||
|
'CHOKIDAR_USEPOLLING',
|
||||||
|
'NPM_CONFIG_REGISTRY',
|
||||||
|
'OCR_IMAGE',
|
||||||
|
'OCR_PORT',
|
||||||
|
'POSTGRES_DB',
|
||||||
|
'POSTGRES_PASSWORD',
|
||||||
|
'POSTGRES_PORT',
|
||||||
|
'POSTGRES_USER',
|
||||||
|
'TENCENT_BROWSER_NOVNC_ENABLED',
|
||||||
|
'TENCENT_BROWSER_NOVNC_PORT',
|
||||||
|
'TENCENT_BROWSER_VNC_PORT',
|
||||||
|
'TZ',
|
||||||
|
'VITE_API_TARGET',
|
||||||
|
]
|
||||||
|
|
||||||
|
const KNOWN_ENV_NAMES = new Set([
|
||||||
|
...ENV_OVERRIDES.map((override) => override.env),
|
||||||
|
...SPECIAL_RUNTIME_ENV_NAMES,
|
||||||
|
...DEPLOYMENT_ONLY_ENV_NAMES,
|
||||||
|
])
|
||||||
|
|
||||||
|
test('env example files only document known runtime or deployment variables', () => {
|
||||||
|
const envFiles = [
|
||||||
|
path.join(WORKSPACE_ROOT, '.env.server.example'),
|
||||||
|
path.join(WORKSPACE_ROOT, '.env.mac-docker.example'),
|
||||||
|
]
|
||||||
|
|
||||||
|
const unknownNames = envFiles.flatMap((filePath) => {
|
||||||
|
return extractExampleEnvNames(filePath)
|
||||||
|
.filter((name) => !KNOWN_ENV_NAMES.has(name))
|
||||||
|
.map((name) => `${path.basename(filePath)}:${name}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(unknownNames, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('compose environment keys only use known runtime or deployment variables', () => {
|
||||||
|
const composeFiles = [
|
||||||
|
path.join(WORKSPACE_ROOT, 'docker-compose.yml'),
|
||||||
|
path.join(WORKSPACE_ROOT, 'docker-compose.dev.yml'),
|
||||||
|
]
|
||||||
|
|
||||||
|
const unknownNames = composeFiles.flatMap((filePath) => {
|
||||||
|
return extractComposeEnvironmentKeys(filePath)
|
||||||
|
.filter((name) => !KNOWN_ENV_NAMES.has(name))
|
||||||
|
.map((name) => `${path.basename(filePath)}:${name}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(unknownNames, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
function extractExampleEnvNames(filePath: string): string[] {
|
||||||
|
const text = fs.readFileSync(filePath, 'utf8')
|
||||||
|
return [...text.matchAll(/^([A-Z][A-Z0-9_]*)=/gm)].map((match) => match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractComposeEnvironmentKeys(filePath: string): string[] {
|
||||||
|
const text = fs.readFileSync(filePath, 'utf8')
|
||||||
|
return [...text.matchAll(/^\s{6}([A-Z][A-Z0-9_]*):/gm)].map((match) => match[1])
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { applyEnvOverrides, ENV_OVERRIDES } from './env-overrides.js'
|
||||||
|
import type { RuntimeConfig } from '../types/runtime-config.js'
|
||||||
|
|
||||||
|
test('applyEnvOverrides maps typed environment values without mutating base config', () => {
|
||||||
|
const baseConfig = createRuntimeConfig()
|
||||||
|
|
||||||
|
const config = applyEnvOverrides(baseConfig, {
|
||||||
|
PORT: '4000',
|
||||||
|
TENCENT_BROWSER_HEADLESS: 'true',
|
||||||
|
TENCENT_BROWSER_SLOW_MO: '250',
|
||||||
|
TENCENT_BROWSER_VNC_RESOLUTION: '1920x1080x24',
|
||||||
|
DATA_ROOT: './tmp/backend-data',
|
||||||
|
DATABASE_SSL: 'yes',
|
||||||
|
ADMIN_DEFAULT_USERS_JSON: '[{"username":"admin","password":"secret","role":"admin"}]',
|
||||||
|
AGISO_TRADE_DETAIL_TIMEOUT_MS: '7000',
|
||||||
|
CLOUDTENTACLES_DEVICE_TYPE: '3',
|
||||||
|
TENCENT_REDEEM_PROOF_MODE: 'basic',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(config.server.port, 4000)
|
||||||
|
assert.equal(config.browser.headless, true)
|
||||||
|
assert.equal(config.browser.slowMoMs, 250)
|
||||||
|
assert.equal(config.browser.vncResolution, '1920x1080x24')
|
||||||
|
assert.equal(config.data.root, path.resolve('./tmp/backend-data'))
|
||||||
|
assert.equal(config.database.ssl, true)
|
||||||
|
assert.deepEqual(config.admin.defaultUsers, [
|
||||||
|
{ username: 'admin', password: 'secret', role: 'admin' },
|
||||||
|
])
|
||||||
|
assert.equal(config.platforms.agiso.tradeDetail.timeoutMs, 7000)
|
||||||
|
assert.equal(config.platforms.cloudtentacles.deviceType, 3)
|
||||||
|
assert.equal(config.redeem.proofMode, 'basic')
|
||||||
|
|
||||||
|
assert.equal(baseConfig.server.port, 3000)
|
||||||
|
assert.equal(baseConfig.browser.headless, null)
|
||||||
|
assert.deepEqual(baseConfig.admin.defaultUsers, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('applyEnvOverrides derives claim base url from app base url when explicit claim url is absent', () => {
|
||||||
|
const config = applyEnvOverrides(createRuntimeConfig(), {
|
||||||
|
APP_BASE_URL: 'https://example.test///',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(config.orders.claimBaseUrl, 'https://example.test/#/claim')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('applyEnvOverrides prefers explicit claim base url over app base url', () => {
|
||||||
|
const config = applyEnvOverrides(createRuntimeConfig(), {
|
||||||
|
APP_BASE_URL: 'https://example.test',
|
||||||
|
CLAIM_BASE_URL: 'https://claim.example.test/custom',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(config.orders.claimBaseUrl, 'https://claim.example.test/custom')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('applyEnvOverrides ignores empty or invalid values', () => {
|
||||||
|
const config = applyEnvOverrides(createRuntimeConfig(), {
|
||||||
|
PORT: 'not-a-number',
|
||||||
|
DATABASE_SSL: 'maybe',
|
||||||
|
ADMIN_DEFAULT_USERS_JSON: '{"username":"admin"}',
|
||||||
|
LOG_LEVEL: ' ',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(config.server.port, 3000)
|
||||||
|
assert.equal(config.database.ssl, false)
|
||||||
|
assert.deepEqual(config.admin.defaultUsers, [])
|
||||||
|
assert.equal(config.logging.level, 'info')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ENV_OVERRIDES keeps environment variable names unique', () => {
|
||||||
|
const names = ENV_OVERRIDES.map((override) => override.env)
|
||||||
|
assert.equal(new Set(names).size, names.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
function createRuntimeConfig(): RuntimeConfig {
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
browser: {
|
||||||
|
chromePath: '',
|
||||||
|
headless: null,
|
||||||
|
devtools: null,
|
||||||
|
keepAlive: null,
|
||||||
|
prewarm: null,
|
||||||
|
slowMoMs: null,
|
||||||
|
vncResolution: '1600x900x24',
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
ocr: {
|
||||||
|
baseUrl: '',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
root: '/tmp/order-site',
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
level: 'info',
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
url: 'postgres://postgres:postgres@127.0.0.1:5432/order_site',
|
||||||
|
ssl: false,
|
||||||
|
maxConnections: 10,
|
||||||
|
},
|
||||||
|
orders: {
|
||||||
|
claimBaseUrl: 'http://127.0.0.1:5173/#/claim',
|
||||||
|
tokenTtlHours: 24,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
sessionSecret: '',
|
||||||
|
sessionTtlHours: 12,
|
||||||
|
defaultUsers: [],
|
||||||
|
},
|
||||||
|
platforms: {
|
||||||
|
agiso: {
|
||||||
|
appSecret: '',
|
||||||
|
tradeDetail: {
|
||||||
|
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/Detail',
|
||||||
|
apiVersion: '1',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
},
|
||||||
|
autoDelivery: {
|
||||||
|
enabled: true,
|
||||||
|
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/DummySend',
|
||||||
|
apiVersion: '1',
|
||||||
|
},
|
||||||
|
messaging: {
|
||||||
|
enabled: false,
|
||||||
|
sendMessageEndpoint: 'https://gw-api.agiso.com/aldsIdle/ImMsg/SendMsg',
|
||||||
|
apiVersion: '1',
|
||||||
|
messageTemplate: '',
|
||||||
|
autoDeliveryMessageTemplate: '',
|
||||||
|
shops: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ninetyone: {
|
||||||
|
userId: '',
|
||||||
|
secret: '',
|
||||||
|
version: '1.0',
|
||||||
|
shopId: '91kaquan',
|
||||||
|
shopName: '91卡券',
|
||||||
|
timestampToleranceSeconds: 600,
|
||||||
|
cardsEncoding: 'aes-256-ecb-base64',
|
||||||
|
},
|
||||||
|
cloudtentacles: {
|
||||||
|
baseUrl: 'https://123.207.217.176',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
sendSmsPath: '/public/verif_code',
|
||||||
|
loginPath: '/public/login',
|
||||||
|
userInfoPath: '/user/info',
|
||||||
|
assetPath: '/user/get_asset',
|
||||||
|
permissionPath: '/user/get_permission',
|
||||||
|
categoriesPath: '/categories/get',
|
||||||
|
skuListPath: '/sku/list',
|
||||||
|
skuBuyPath: '/sku/buy',
|
||||||
|
skuUsePath: '/sku/use',
|
||||||
|
knapsackPath: '/user/get_knapsack',
|
||||||
|
vnListPath: '/vn/list',
|
||||||
|
vnAppointPath: '/vn/appoint',
|
||||||
|
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
||||||
|
vnVerifCodePath: '/public/vn_verif_code',
|
||||||
|
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||||
|
vnBindUrlPath: '/vn/bind_url',
|
||||||
|
vnBindInfoPath: '/vn/bind_info',
|
||||||
|
vnBackPath: '/vn/back',
|
||||||
|
bindUrlTtlSeconds: 600,
|
||||||
|
bindUrlProbeIntervalSeconds: 30,
|
||||||
|
bindUrlProbeTimeoutMs: 5000,
|
||||||
|
bindUrlProbeUserAgent: '',
|
||||||
|
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
||||||
|
bindUrlProbeChartId: '323794',
|
||||||
|
bindUrlProbeSubChartId: '323794',
|
||||||
|
bindUrlProbeIdeToken: 'z90Syo',
|
||||||
|
bindUrlProbeActivityUrl: '',
|
||||||
|
bindUrlProbeReferer: 'https://gp.qq.com/',
|
||||||
|
bindUrlProbeExtraCookie: '',
|
||||||
|
publicKeyPem: '',
|
||||||
|
clientSource: 'ct-client',
|
||||||
|
deviceId: '-',
|
||||||
|
deviceType: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redeem: {
|
||||||
|
proofMode: 'full',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
import type { AdminDefaultUser, RuntimeConfig } from '../types/runtime-config.js'
|
||||||
|
import { parseBoolean, parseInteger, parseJsonArray } from './runtime-env.js'
|
||||||
|
import { deepMerge } from './runtime-merge.js'
|
||||||
|
|
||||||
|
type RuntimeConfigPath = readonly [string, ...string[]]
|
||||||
|
type RuntimeConfigValue = string | number | boolean | AdminDefaultUser[]
|
||||||
|
type RuntimeEnv = Record<string, string | undefined>
|
||||||
|
|
||||||
|
type EnvOverride = {
|
||||||
|
env: string
|
||||||
|
path: RuntimeConfigPath
|
||||||
|
read: (rawValue: unknown) => RuntimeConfigValue | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENV_OVERRIDES: readonly EnvOverride[] = [
|
||||||
|
integerEnv('PORT', ['server', 'port']),
|
||||||
|
stringEnv('CHROME_PATH', ['browser', 'chromePath']),
|
||||||
|
booleanEnv('TENCENT_BROWSER_HEADLESS', ['browser', 'headless']),
|
||||||
|
booleanEnv('TENCENT_BROWSER_DEVTOOLS', ['browser', 'devtools']),
|
||||||
|
booleanEnv('TENCENT_BROWSER_KEEP_ALIVE', ['browser', 'keepAlive']),
|
||||||
|
booleanEnv('TENCENT_BROWSER_PREWARM', ['browser', 'prewarm']),
|
||||||
|
integerEnv('TENCENT_BROWSER_SLOW_MO', ['browser', 'slowMoMs']),
|
||||||
|
stringEnv('TENCENT_BROWSER_VNC_RESOLUTION', ['browser', 'vncResolution']),
|
||||||
|
booleanEnv('TENCENT_SESSION_DEBUG', ['session', 'debug']),
|
||||||
|
stringEnv('OCR_BASE_URL', ['ocr', 'baseUrl']),
|
||||||
|
stringEnv('DATA_ROOT', ['data', 'root'], path.resolve),
|
||||||
|
stringEnv('LOG_LEVEL', ['logging', 'level']),
|
||||||
|
stringEnv('DATABASE_URL', ['database', 'url']),
|
||||||
|
booleanEnv('DATABASE_SSL', ['database', 'ssl']),
|
||||||
|
integerEnv('DATABASE_MAX_CONNECTIONS', ['database', 'maxConnections']),
|
||||||
|
integerEnv('CLAIM_TOKEN_TTL_HOURS', ['orders', 'tokenTtlHours']),
|
||||||
|
stringEnv('ADMIN_SESSION_SECRET', ['admin', 'sessionSecret']),
|
||||||
|
integerEnv('ADMIN_SESSION_TTL_HOURS', ['admin', 'sessionTtlHours']),
|
||||||
|
adminUsersJsonEnv('ADMIN_DEFAULT_USERS_JSON', ['admin', 'defaultUsers']),
|
||||||
|
stringEnv('AGISO_APP_SECRET', ['platforms', 'agiso', 'appSecret']),
|
||||||
|
stringEnv('AGISO_TRADE_DETAIL_ENDPOINT', ['platforms', 'agiso', 'tradeDetail', 'endpoint']),
|
||||||
|
stringEnv('AGISO_TRADE_DETAIL_API_VERSION', ['platforms', 'agiso', 'tradeDetail', 'apiVersion']),
|
||||||
|
integerEnv('AGISO_TRADE_DETAIL_TIMEOUT_MS', ['platforms', 'agiso', 'tradeDetail', 'timeoutMs']),
|
||||||
|
booleanEnv('AGISO_AUTO_DELIVERY_ENABLED', ['platforms', 'agiso', 'autoDelivery', 'enabled']),
|
||||||
|
stringEnv('AGISO_AUTO_DELIVERY_ENDPOINT', ['platforms', 'agiso', 'autoDelivery', 'endpoint']),
|
||||||
|
stringEnv('AGISO_AUTO_DELIVERY_API_VERSION', ['platforms', 'agiso', 'autoDelivery', 'apiVersion']),
|
||||||
|
stringEnv('AGISO_MESSAGE_API_VERSION', ['platforms', 'agiso', 'messaging', 'apiVersion']),
|
||||||
|
stringEnv('AGISO_SEND_MESSAGE_ENDPOINT', ['platforms', 'agiso', 'messaging', 'sendMessageEndpoint']),
|
||||||
|
booleanEnv('AGISO_MESSAGING_ENABLED', ['platforms', 'agiso', 'messaging', 'enabled']),
|
||||||
|
stringEnv('NINETYONE_USER_ID', ['platforms', 'ninetyone', 'userId']),
|
||||||
|
stringEnv('NINETYONE_SECRET', ['platforms', 'ninetyone', 'secret']),
|
||||||
|
stringEnv('NINETYONE_VERSION', ['platforms', 'ninetyone', 'version']),
|
||||||
|
stringEnv('NINETYONE_SHOP_ID', ['platforms', 'ninetyone', 'shopId']),
|
||||||
|
stringEnv('NINETYONE_SHOP_NAME', ['platforms', 'ninetyone', 'shopName']),
|
||||||
|
integerEnv('NINETYONE_TIMESTAMP_TOLERANCE_SECONDS', ['platforms', 'ninetyone', 'timestampToleranceSeconds']),
|
||||||
|
stringEnv('NINETYONE_CARDS_ENCODING', ['platforms', 'ninetyone', 'cardsEncoding']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BASE_URL', ['platforms', 'cloudtentacles', 'baseUrl']),
|
||||||
|
integerEnv('CLOUDTENTACLES_TIMEOUT_MS', ['platforms', 'cloudtentacles', 'timeoutMs']),
|
||||||
|
stringEnv('CLOUDTENTACLES_SEND_SMS_PATH', ['platforms', 'cloudtentacles', 'sendSmsPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_LOGIN_PATH', ['platforms', 'cloudtentacles', 'loginPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_USER_INFO_PATH', ['platforms', 'cloudtentacles', 'userInfoPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_ASSET_PATH', ['platforms', 'cloudtentacles', 'assetPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_PERMISSION_PATH', ['platforms', 'cloudtentacles', 'permissionPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_CATEGORIES_PATH', ['platforms', 'cloudtentacles', 'categoriesPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_SKU_LIST_PATH', ['platforms', 'cloudtentacles', 'skuListPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_SKU_BUY_PATH', ['platforms', 'cloudtentacles', 'skuBuyPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_KNAPSACK_PATH', ['platforms', 'cloudtentacles', 'knapsackPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_LIST_PATH', ['platforms', 'cloudtentacles', 'vnListPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_APPOINT_PATH', ['platforms', 'cloudtentacles', 'vnAppointPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH', ['platforms', 'cloudtentacles', 'vnGenerateLoginCodePath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_VERIF_CODE_PATH', ['platforms', 'cloudtentacles', 'vnVerifCodePath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH', ['platforms', 'cloudtentacles', 'vnVerifyLoginCodePath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_BIND_URL_PATH', ['platforms', 'cloudtentacles', 'vnBindUrlPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_BIND_INFO_PATH', ['platforms', 'cloudtentacles', 'vnBindInfoPath']),
|
||||||
|
stringEnv('CLOUDTENTACLES_VN_BACK_PATH', ['platforms', 'cloudtentacles', 'vnBackPath']),
|
||||||
|
integerEnv('CLOUDTENTACLES_BIND_URL_TTL_SECONDS', ['platforms', 'cloudtentacles', 'bindUrlTtlSeconds']),
|
||||||
|
integerEnv('CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS', ['platforms', 'cloudtentacles', 'bindUrlProbeIntervalSeconds']),
|
||||||
|
integerEnv('CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS', ['platforms', 'cloudtentacles', 'bindUrlProbeTimeoutMs']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_USER_AGENT', ['platforms', 'cloudtentacles', 'bindUrlProbeUserAgent']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_ENDPOINT', ['platforms', 'cloudtentacles', 'bindUrlProbeEndpoint']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_CHART_ID', ['platforms', 'cloudtentacles', 'bindUrlProbeChartId']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_SUB_CHART_ID', ['platforms', 'cloudtentacles', 'bindUrlProbeSubChartId']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_IDE_TOKEN', ['platforms', 'cloudtentacles', 'bindUrlProbeIdeToken']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL', ['platforms', 'cloudtentacles', 'bindUrlProbeActivityUrl']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_REFERER', ['platforms', 'cloudtentacles', 'bindUrlProbeReferer']),
|
||||||
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_EXTRA_COOKIE', ['platforms', 'cloudtentacles', 'bindUrlProbeExtraCookie']),
|
||||||
|
stringEnv('CLOUDTENTACLES_PUBLIC_KEY_PEM', ['platforms', 'cloudtentacles', 'publicKeyPem']),
|
||||||
|
stringEnv('CLOUDTENTACLES_CLIENT_SOURCE', ['platforms', 'cloudtentacles', 'clientSource']),
|
||||||
|
stringEnv('CLOUDTENTACLES_DEVICE_ID', ['platforms', 'cloudtentacles', 'deviceId']),
|
||||||
|
integerEnv('CLOUDTENTACLES_DEVICE_TYPE', ['platforms', 'cloudtentacles', 'deviceType']),
|
||||||
|
stringEnv('TENCENT_REDEEM_PROOF_MODE', ['redeem', 'proofMode']),
|
||||||
|
]
|
||||||
|
|
||||||
|
export function applyEnvOverrides(baseConfig: RuntimeConfig, env: RuntimeEnv = process.env): RuntimeConfig {
|
||||||
|
const nextConfig = deepMerge(baseConfig, {})
|
||||||
|
|
||||||
|
applyClaimBaseUrlOverride(nextConfig, env)
|
||||||
|
|
||||||
|
for (const override of ENV_OVERRIDES) {
|
||||||
|
const value = override.read(env[override.env])
|
||||||
|
if (value !== null) {
|
||||||
|
setConfigValue(nextConfig, override.path, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyClaimBaseUrlOverride(config: RuntimeConfig, env: RuntimeEnv): void {
|
||||||
|
const claimBaseUrl = parseString(env.CLAIM_BASE_URL)
|
||||||
|
if (claimBaseUrl !== null) {
|
||||||
|
config.orders.claimBaseUrl = claimBaseUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appBaseUrl = parseString(env.APP_BASE_URL)
|
||||||
|
if (appBaseUrl !== null) {
|
||||||
|
config.orders.claimBaseUrl = `${appBaseUrl.replace(/\/+$/, '')}/#/claim`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringEnv(
|
||||||
|
env: string,
|
||||||
|
configPath: RuntimeConfigPath,
|
||||||
|
transform: (value: string) => string = (value) => value,
|
||||||
|
): EnvOverride {
|
||||||
|
return {
|
||||||
|
env,
|
||||||
|
path: configPath,
|
||||||
|
read(rawValue) {
|
||||||
|
const value = parseString(rawValue)
|
||||||
|
return value === null ? null : transform(value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function integerEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||||
|
return {
|
||||||
|
env,
|
||||||
|
path: configPath,
|
||||||
|
read: parseInteger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function booleanEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||||
|
return {
|
||||||
|
env,
|
||||||
|
path: configPath,
|
||||||
|
read: parseBoolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminUsersJsonEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||||
|
return {
|
||||||
|
env,
|
||||||
|
path: configPath,
|
||||||
|
read(rawValue) {
|
||||||
|
return parseJsonArray<AdminDefaultUser>(rawValue)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseString(rawValue: unknown): string | null {
|
||||||
|
const value = String(rawValue || '').trim()
|
||||||
|
return value ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigValue(config: RuntimeConfig, configPath: RuntimeConfigPath, value: RuntimeConfigValue): void {
|
||||||
|
let current: Record<string, unknown> = config as unknown as Record<string, unknown>
|
||||||
|
|
||||||
|
for (const key of configPath.slice(0, -1)) {
|
||||||
|
const nextValue = current[key]
|
||||||
|
if (!nextValue || typeof nextValue !== 'object' || Array.isArray(nextValue)) {
|
||||||
|
current[key] = {}
|
||||||
|
}
|
||||||
|
current = current[key] as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
current[configPath[configPath.length - 1]] = value
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
import { createRequire } from 'node:module'
|
|
||||||
|
|
||||||
import { isPlainObject } from './runtime-merge.js'
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url)
|
|
||||||
|
|
||||||
export function loadConfig(configPath: string): Record<string, unknown> {
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loaded = require(configPath)
|
|
||||||
return isPlainObject(loaded) ? loaded : {}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import {
|
||||||
|
RuntimeConfigValidationError,
|
||||||
|
assertRuntimeConfigValid,
|
||||||
|
validateRuntimeConfig,
|
||||||
|
} from './runtime-validation.js'
|
||||||
|
import { deepMerge } from './runtime-merge.js'
|
||||||
|
import type { RuntimeConfig } from '../types/runtime-config.js'
|
||||||
|
|
||||||
|
test('validateRuntimeConfig allows missing admin credentials outside production', () => {
|
||||||
|
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||||
|
admin: {
|
||||||
|
sessionSecret: '',
|
||||||
|
defaultUsers: [],
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
env: { NODE_ENV: 'development' },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(issues, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateRuntimeConfig requires admin credentials in production', () => {
|
||||||
|
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||||
|
admin: {
|
||||||
|
sessionSecret: '',
|
||||||
|
defaultUsers: [],
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
env: { NODE_ENV: 'production' },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(issues.map((issue) => issue.path), [
|
||||||
|
'admin.sessionSecret',
|
||||||
|
'admin.defaultUsers',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateRuntimeConfig rejects unsafe production placeholders', () => {
|
||||||
|
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||||
|
admin: {
|
||||||
|
sessionSecret: 'change-me-long-random-session-secret',
|
||||||
|
defaultUsers: [
|
||||||
|
{
|
||||||
|
username: 'admin',
|
||||||
|
password: 'change-me-admin-password',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
env: { NODE_ENV: 'production' },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(issues.map((issue) => issue.path), [
|
||||||
|
'admin.sessionSecret',
|
||||||
|
'admin.defaultUsers[0].password',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateRuntimeConfig accepts complete production config', () => {
|
||||||
|
const issues = validateRuntimeConfig(createRuntimeConfig(), {
|
||||||
|
env: { NODE_ENV: 'production' },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(issues, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateRuntimeConfig catches invalid numeric and url values', () => {
|
||||||
|
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||||
|
server: {
|
||||||
|
port: 70000,
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
maxConnections: 0,
|
||||||
|
},
|
||||||
|
orders: {
|
||||||
|
claimBaseUrl: 'not-a-url',
|
||||||
|
tokenTtlHours: 0,
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
env: { NODE_ENV: 'development' },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(issues.map((issue) => issue.path), [
|
||||||
|
'server.port',
|
||||||
|
'database.maxConnections',
|
||||||
|
'orders.tokenTtlHours',
|
||||||
|
'orders.claimBaseUrl',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assertRuntimeConfigValid throws a readable validation error', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
assertRuntimeConfigValid(createRuntimeConfig({
|
||||||
|
server: {
|
||||||
|
port: 0,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}, (error) => {
|
||||||
|
assert.equal(error instanceof RuntimeConfigValidationError, true)
|
||||||
|
assert.match((error as Error).message, /运行时配置校验失败/)
|
||||||
|
assert.match((error as Error).message, /server\.port/)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function createRuntimeConfig(overrides: Record<string, unknown> = {}): RuntimeConfig {
|
||||||
|
return deepMerge({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
browser: {
|
||||||
|
chromePath: '',
|
||||||
|
headless: null,
|
||||||
|
devtools: null,
|
||||||
|
keepAlive: null,
|
||||||
|
prewarm: null,
|
||||||
|
slowMoMs: null,
|
||||||
|
vncResolution: '1600x900x24',
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
ocr: {
|
||||||
|
baseUrl: '',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
root: '/tmp/order-site',
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
level: 'info',
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
url: 'postgres://postgres:postgres@postgres:5432/order_site',
|
||||||
|
ssl: false,
|
||||||
|
maxConnections: 10,
|
||||||
|
},
|
||||||
|
orders: {
|
||||||
|
claimBaseUrl: 'https://order.example.test/#/claim',
|
||||||
|
tokenTtlHours: 24,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
sessionSecret: '0123456789abcdef0123456789abcdef',
|
||||||
|
sessionTtlHours: 12,
|
||||||
|
defaultUsers: [
|
||||||
|
{
|
||||||
|
username: 'admin',
|
||||||
|
password: 'Qp7xT2mN9vR4',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
platforms: {
|
||||||
|
agiso: {
|
||||||
|
appSecret: '',
|
||||||
|
tradeDetail: {
|
||||||
|
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/Detail',
|
||||||
|
apiVersion: '1',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
},
|
||||||
|
autoDelivery: {
|
||||||
|
enabled: true,
|
||||||
|
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/DummySend',
|
||||||
|
apiVersion: '1',
|
||||||
|
},
|
||||||
|
messaging: {
|
||||||
|
enabled: false,
|
||||||
|
sendMessageEndpoint: 'https://gw-api.agiso.com/aldsIdle/ImMsg/SendMsg',
|
||||||
|
apiVersion: '1',
|
||||||
|
messageTemplate: '',
|
||||||
|
autoDeliveryMessageTemplate: '',
|
||||||
|
shops: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ninetyone: {
|
||||||
|
userId: '',
|
||||||
|
secret: '',
|
||||||
|
version: '1.0',
|
||||||
|
shopId: '91kaquan',
|
||||||
|
shopName: '91卡券',
|
||||||
|
timestampToleranceSeconds: 600,
|
||||||
|
cardsEncoding: 'aes-256-ecb-base64',
|
||||||
|
},
|
||||||
|
cloudtentacles: {
|
||||||
|
baseUrl: 'https://123.207.217.176',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
sendSmsPath: '/public/verif_code',
|
||||||
|
loginPath: '/public/login',
|
||||||
|
userInfoPath: '/user/info',
|
||||||
|
assetPath: '/user/get_asset',
|
||||||
|
permissionPath: '/user/get_permission',
|
||||||
|
categoriesPath: '/categories/get',
|
||||||
|
skuListPath: '/sku/list',
|
||||||
|
skuBuyPath: '/sku/buy',
|
||||||
|
skuUsePath: '/sku/use',
|
||||||
|
knapsackPath: '/user/get_knapsack',
|
||||||
|
vnListPath: '/vn/list',
|
||||||
|
vnAppointPath: '/vn/appoint',
|
||||||
|
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
||||||
|
vnVerifCodePath: '/public/vn_verif_code',
|
||||||
|
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||||
|
vnBindUrlPath: '/vn/bind_url',
|
||||||
|
vnBindInfoPath: '/vn/bind_info',
|
||||||
|
vnBackPath: '/vn/back',
|
||||||
|
bindUrlTtlSeconds: 600,
|
||||||
|
bindUrlProbeIntervalSeconds: 30,
|
||||||
|
bindUrlProbeTimeoutMs: 5000,
|
||||||
|
bindUrlProbeUserAgent: '',
|
||||||
|
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
||||||
|
bindUrlProbeChartId: '323794',
|
||||||
|
bindUrlProbeSubChartId: '323794',
|
||||||
|
bindUrlProbeIdeToken: 'z90Syo',
|
||||||
|
bindUrlProbeActivityUrl: '',
|
||||||
|
bindUrlProbeReferer: 'https://gp.qq.com/',
|
||||||
|
bindUrlProbeExtraCookie: '',
|
||||||
|
publicKeyPem: '',
|
||||||
|
clientSource: 'ct-client',
|
||||||
|
deviceId: '-',
|
||||||
|
deviceType: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redeem: {
|
||||||
|
proofMode: 'full',
|
||||||
|
},
|
||||||
|
}, overrides) as RuntimeConfig
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
import type { AdminDefaultUser, RuntimeConfig } from '../types/runtime-config.js'
|
||||||
|
|
||||||
|
type RuntimeEnvironment = {
|
||||||
|
NODE_ENV?: string
|
||||||
|
CI?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationOptions = {
|
||||||
|
env?: RuntimeEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RuntimeConfigValidationIssue = {
|
||||||
|
path: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RuntimeConfigValidationError extends Error {
|
||||||
|
issues: RuntimeConfigValidationIssue[]
|
||||||
|
|
||||||
|
constructor(issues: RuntimeConfigValidationIssue[]) {
|
||||||
|
super(formatRuntimeConfigValidationMessage(issues))
|
||||||
|
this.name = 'RuntimeConfigValidationError'
|
||||||
|
this.issues = issues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertRuntimeConfigValid(
|
||||||
|
config: RuntimeConfig,
|
||||||
|
options: ValidationOptions = {},
|
||||||
|
): void {
|
||||||
|
const issues = validateRuntimeConfig(config, options)
|
||||||
|
if (issues.length > 0) {
|
||||||
|
throw new RuntimeConfigValidationError(issues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateRuntimeConfig(
|
||||||
|
config: RuntimeConfig,
|
||||||
|
{ env = process.env }: ValidationOptions = {},
|
||||||
|
): RuntimeConfigValidationIssue[] {
|
||||||
|
const issues: RuntimeConfigValidationIssue[] = []
|
||||||
|
const productionLike = isProductionLike(env)
|
||||||
|
|
||||||
|
requireInteger(issues, 'server.port', config.server?.port, { min: 1, max: 65535 })
|
||||||
|
requireInteger(issues, 'database.maxConnections', config.database?.maxConnections, { min: 1 })
|
||||||
|
requireInteger(issues, 'orders.tokenTtlHours', config.orders?.tokenTtlHours, { min: 1 })
|
||||||
|
requireInteger(issues, 'admin.sessionTtlHours', config.admin?.sessionTtlHours, { min: 1 })
|
||||||
|
requireInteger(issues, 'platforms.agiso.tradeDetail.timeoutMs', config.platforms?.agiso?.tradeDetail?.timeoutMs, { min: 1 })
|
||||||
|
requireInteger(issues, 'platforms.cloudtentacles.timeoutMs', config.platforms?.cloudtentacles?.timeoutMs, { min: 1 })
|
||||||
|
requireInteger(issues, 'platforms.cloudtentacles.bindUrlTtlSeconds', config.platforms?.cloudtentacles?.bindUrlTtlSeconds, { min: 1 })
|
||||||
|
requireInteger(
|
||||||
|
issues,
|
||||||
|
'platforms.cloudtentacles.bindUrlProbeIntervalSeconds',
|
||||||
|
config.platforms?.cloudtentacles?.bindUrlProbeIntervalSeconds,
|
||||||
|
{ min: 1 },
|
||||||
|
)
|
||||||
|
requireInteger(
|
||||||
|
issues,
|
||||||
|
'platforms.cloudtentacles.bindUrlProbeTimeoutMs',
|
||||||
|
config.platforms?.cloudtentacles?.bindUrlProbeTimeoutMs,
|
||||||
|
{ min: 1 },
|
||||||
|
)
|
||||||
|
|
||||||
|
validateOptionalHttpUrl(issues, 'orders.claimBaseUrl', config.orders?.claimBaseUrl)
|
||||||
|
validateRequiredString(issues, 'data.root', config.data?.root)
|
||||||
|
|
||||||
|
if (productionLike) {
|
||||||
|
validateRequiredString(issues, 'database.url', config.database?.url)
|
||||||
|
validateRequiredHttpUrl(issues, 'orders.claimBaseUrl', config.orders?.claimBaseUrl)
|
||||||
|
validateSecret(issues, 'admin.sessionSecret', config.admin?.sessionSecret, { minLength: 32 })
|
||||||
|
validateAdminDefaultUsers(issues, config.admin?.defaultUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProductionLike(env: RuntimeEnvironment = process.env): boolean {
|
||||||
|
return env.NODE_ENV === 'production'
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAdminDefaultUsers(
|
||||||
|
issues: RuntimeConfigValidationIssue[],
|
||||||
|
users: AdminDefaultUser[] | undefined,
|
||||||
|
): void {
|
||||||
|
if (!Array.isArray(users) || users.length === 0) {
|
||||||
|
issues.push({
|
||||||
|
path: 'admin.defaultUsers',
|
||||||
|
message: '生产环境必须配置至少一个后台默认用户',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
users.forEach((user, index) => {
|
||||||
|
validateRequiredString(issues, `admin.defaultUsers[${index}].username`, user?.username)
|
||||||
|
validateSecret(issues, `admin.defaultUsers[${index}].password`, user?.password, { minLength: 8 })
|
||||||
|
validateRequiredString(issues, `admin.defaultUsers[${index}].role`, user?.role)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequiredString(
|
||||||
|
issues: RuntimeConfigValidationIssue[],
|
||||||
|
configPath: string,
|
||||||
|
value: unknown,
|
||||||
|
): void {
|
||||||
|
if (!String(value || '').trim()) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: '必须配置非空字符串',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSecret(
|
||||||
|
issues: RuntimeConfigValidationIssue[],
|
||||||
|
configPath: string,
|
||||||
|
value: unknown,
|
||||||
|
{ minLength }: { minLength: number },
|
||||||
|
): void {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: '必须配置密钥',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length < minLength) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: `长度不能少于 ${minLength} 个字符`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/change-me|example|password|secret/i.test(text)) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: '不能使用示例占位值',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptionalHttpUrl(
|
||||||
|
issues: RuntimeConfigValidationIssue[],
|
||||||
|
configPath: string,
|
||||||
|
value: unknown,
|
||||||
|
): void {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHttpUrl(text)) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: '必须是 http 或 https URL',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequiredHttpUrl(
|
||||||
|
issues: RuntimeConfigValidationIssue[],
|
||||||
|
configPath: string,
|
||||||
|
value: unknown,
|
||||||
|
): void {
|
||||||
|
validateRequiredString(issues, configPath, value)
|
||||||
|
validateOptionalHttpUrl(issues, configPath, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireInteger(
|
||||||
|
issues: RuntimeConfigValidationIssue[],
|
||||||
|
configPath: string,
|
||||||
|
value: unknown,
|
||||||
|
{ min, max }: { min?: number, max?: number } = {},
|
||||||
|
): void {
|
||||||
|
const numberValue = Number(value)
|
||||||
|
if (!Number.isInteger(numberValue)) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: '必须是整数',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof min === 'number' && numberValue < min) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: `不能小于 ${min}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof max === 'number' && numberValue > max) {
|
||||||
|
issues.push({
|
||||||
|
path: configPath,
|
||||||
|
message: `不能大于 ${max}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHttpUrl(value: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(value)
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRuntimeConfigValidationMessage(issues: RuntimeConfigValidationIssue[]): string {
|
||||||
|
const lines = issues.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||||
|
return `运行时配置校验失败:\n${lines.join('\n')}`
|
||||||
|
}
|
||||||
@@ -1,519 +1,17 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { loadEnvFiles, parseBoolean, parseInteger, parseJsonArray } from "./runtime-env.js";
|
import { createDefaultRuntimeConfig } from "./defaults.js";
|
||||||
import { loadConfig } from "./runtime-loader.js";
|
import { applyEnvOverrides } from "./env-overrides.js";
|
||||||
import { deepMerge } from "./runtime-merge.js";
|
import { loadEnvFiles } from "./runtime-env.js";
|
||||||
import type { AdminDefaultUser, RuntimeConfig } from "../types/runtime-config.js";
|
|
||||||
|
|
||||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||||
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, "../..");
|
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, "../..");
|
||||||
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, "../..");
|
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, "../..");
|
||||||
const CONFIG_ROOT = path.join(PROJECT_ROOT, "config");
|
|
||||||
|
|
||||||
loadEnvFiles([
|
loadEnvFiles([
|
||||||
path.join(WORKSPACE_ROOT, ".env"),
|
path.join(WORKSPACE_ROOT, ".env"),
|
||||||
path.join(PROJECT_ROOT, ".env"),
|
path.join(PROJECT_ROOT, ".env"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const defaultConfig = loadConfig(path.join(CONFIG_ROOT, "default.cjs")) as RuntimeConfig;
|
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT));
|
||||||
|
|
||||||
export const runtimeConfig = applyEnvOverrides(defaultConfig);
|
|
||||||
|
|
||||||
function applyEnvOverrides(baseConfig: RuntimeConfig): RuntimeConfig {
|
|
||||||
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 ocrBaseUrl = String(process.env.OCR_BASE_URL || "").trim();
|
|
||||||
if (ocrBaseUrl) {
|
|
||||||
nextConfig.ocr.baseUrl = ocrBaseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<AdminDefaultUser>(
|
|
||||||
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 ninetyoneUserId = String(process.env.NINETYONE_USER_ID || "").trim();
|
|
||||||
if (ninetyoneUserId) {
|
|
||||||
nextConfig.platforms.ninetyone.userId = ninetyoneUserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ninetyoneSecret = String(process.env.NINETYONE_SECRET || "").trim();
|
|
||||||
if (ninetyoneSecret) {
|
|
||||||
nextConfig.platforms.ninetyone.secret = ninetyoneSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ninetyoneVersion = String(process.env.NINETYONE_VERSION || "").trim();
|
|
||||||
if (ninetyoneVersion) {
|
|
||||||
nextConfig.platforms.ninetyone.version = ninetyoneVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ninetyoneShopId = String(process.env.NINETYONE_SHOP_ID || "").trim();
|
|
||||||
if (ninetyoneShopId) {
|
|
||||||
nextConfig.platforms.ninetyone.shopId = ninetyoneShopId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ninetyoneShopName = String(
|
|
||||||
process.env.NINETYONE_SHOP_NAME || ""
|
|
||||||
).trim();
|
|
||||||
if (ninetyoneShopName) {
|
|
||||||
nextConfig.platforms.ninetyone.shopName = ninetyoneShopName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ninetyoneTimestampToleranceSeconds = parseInteger(
|
|
||||||
process.env.NINETYONE_TIMESTAMP_TOLERANCE_SECONDS
|
|
||||||
);
|
|
||||||
if (ninetyoneTimestampToleranceSeconds !== null) {
|
|
||||||
nextConfig.platforms.ninetyone.timestampToleranceSeconds =
|
|
||||||
ninetyoneTimestampToleranceSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ninetyoneCardsEncoding = String(
|
|
||||||
process.env.NINETYONE_CARDS_ENCODING || ""
|
|
||||||
).trim();
|
|
||||||
if (ninetyoneCardsEncoding) {
|
|
||||||
nextConfig.platforms.ninetyone.cardsEncoding = ninetyoneCardsEncoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 cloudtentaclesVnBindInfoPath = String(
|
|
||||||
process.env.CLOUDTENTACLES_VN_BIND_INFO_PATH || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesVnBindInfoPath) {
|
|
||||||
nextConfig.platforms.cloudtentacles.vnBindInfoPath =
|
|
||||||
cloudtentaclesVnBindInfoPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesVnBackPath = String(
|
|
||||||
process.env.CLOUDTENTACLES_VN_BACK_PATH || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesVnBackPath) {
|
|
||||||
nextConfig.platforms.cloudtentacles.vnBackPath = cloudtentaclesVnBackPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlTtlSeconds = parseInteger(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_TTL_SECONDS
|
|
||||||
);
|
|
||||||
if (cloudtentaclesBindUrlTtlSeconds !== null) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlTtlSeconds =
|
|
||||||
cloudtentaclesBindUrlTtlSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeIntervalSeconds = parseInteger(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS
|
|
||||||
);
|
|
||||||
if (cloudtentaclesBindUrlProbeIntervalSeconds !== null) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeIntervalSeconds =
|
|
||||||
cloudtentaclesBindUrlProbeIntervalSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeTimeoutMs = parseInteger(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
if (cloudtentaclesBindUrlProbeTimeoutMs !== null) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeTimeoutMs =
|
|
||||||
cloudtentaclesBindUrlProbeTimeoutMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeUserAgent = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_USER_AGENT || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeUserAgent) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeUserAgent =
|
|
||||||
cloudtentaclesBindUrlProbeUserAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeEndpoint = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_ENDPOINT || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeEndpoint) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeEndpoint =
|
|
||||||
cloudtentaclesBindUrlProbeEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeChartId = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_CHART_ID || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeChartId) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeChartId =
|
|
||||||
cloudtentaclesBindUrlProbeChartId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeSubChartId = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_SUB_CHART_ID || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeSubChartId) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeSubChartId =
|
|
||||||
cloudtentaclesBindUrlProbeSubChartId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeIdeToken = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_IDE_TOKEN || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeIdeToken) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeIdeToken =
|
|
||||||
cloudtentaclesBindUrlProbeIdeToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeActivityUrl = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeActivityUrl) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeActivityUrl =
|
|
||||||
cloudtentaclesBindUrlProbeActivityUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeReferer = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_REFERER || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeReferer) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeReferer =
|
|
||||||
cloudtentaclesBindUrlProbeReferer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBindUrlProbeExtraCookie = String(
|
|
||||||
process.env.CLOUDTENTACLES_BIND_URL_PROBE_EXTRA_COOKIE || ""
|
|
||||||
).trim();
|
|
||||||
if (cloudtentaclesBindUrlProbeExtraCookie) {
|
|
||||||
nextConfig.platforms.cloudtentacles.bindUrlProbeExtraCookie =
|
|
||||||
cloudtentaclesBindUrlProbeExtraCookie;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import process from "node:process";
|
|||||||
|
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { runtimeConfig } from "./config/runtime.js";
|
import { runtimeConfig } from "./config/runtime.js";
|
||||||
|
import { assertRuntimeConfigValid } from "./config/runtime-validation.js";
|
||||||
import { bootstrapCoreServices } from "./startup/bootstrap.js";
|
import { bootstrapCoreServices } from "./startup/bootstrap.js";
|
||||||
import { createShutdownController } from "./startup/shutdown.js";
|
import { createShutdownController } from "./startup/shutdown.js";
|
||||||
import { createStartupState, formatStartupError } from "./startup/state.js";
|
import { createStartupState, formatStartupError } from "./startup/state.js";
|
||||||
@@ -13,6 +14,13 @@ const startupState = createStartupState();
|
|||||||
|
|
||||||
let shutdownController: ReturnType<typeof createShutdownController> | null = null;
|
let shutdownController: ReturnType<typeof createShutdownController> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
assertRuntimeConfigValid(runtimeConfig);
|
||||||
|
} catch (error) {
|
||||||
|
logError("[startup]", "运行时配置校验失败,服务停止启动", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
startupState,
|
startupState,
|
||||||
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
|
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function buildChromiumLaunchOptions(
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDefaultViewport(rawValue = process.env.TENCENT_BROWSER_VNC_RESOLUTION): ViewportSize {
|
export function resolveDefaultViewport(rawValue?: unknown): ViewportSize {
|
||||||
const fromVncResolution = parseViewportSpec(rawValue)
|
const fromVncResolution = parseViewportSpec(rawValue)
|
||||||
if (fromVncResolution) {
|
if (fromVncResolution) {
|
||||||
return fromVncResolution
|
return fromVncResolution
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
import { runtimeConfig } from '../../config/runtime.js'
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
import { resolveDefaultViewport } from './session-browser-config.js'
|
import { resolveDefaultViewport } from './session-browser-config.js'
|
||||||
|
|
||||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
export const BROWSER_SESSION_DATA_ROOT = path.join(runtimeConfig.data.root, 'browser-sessions')
|
||||||
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..')
|
|
||||||
|
|
||||||
export const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions')
|
|
||||||
export const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
|
export const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
|
||||||
export const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
|
export const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
|
||||||
export const REDEEM_SUCCESS_AUTO_CLOSE_MS = 3 * 60 * 1000
|
export const REDEEM_SUCCESS_AUTO_CLOSE_MS = 3 * 60 * 1000
|
||||||
export const DEFAULT_VIEWPORT = resolveDefaultViewport()
|
export const DEFAULT_VIEWPORT = resolveDefaultViewport(runtimeConfig.browser.vncResolution)
|
||||||
export const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
export const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DATA_ROOT,
|
BROWSER_SESSION_DATA_ROOT,
|
||||||
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
||||||
} from './session-service-config.js'
|
} from './session-service-config.js'
|
||||||
import { SessionServiceError } from './session-service-runtime.js'
|
import { SessionServiceError } from './session-service-runtime.js'
|
||||||
@@ -61,7 +61,7 @@ export async function registerTencentBrowserSession({
|
|||||||
page,
|
page,
|
||||||
}: RegisterSessionInput): Promise<TencentBrowserSession> {
|
}: RegisterSessionInput): Promise<TencentBrowserSession> {
|
||||||
const sessionId = createSessionId()
|
const sessionId = createSessionId()
|
||||||
const sessionDir = path.join(DATA_ROOT, sessionId)
|
const sessionDir = path.join(BROWSER_SESSION_DATA_ROOT, sessionId)
|
||||||
await fs.mkdir(sessionDir, { recursive: true })
|
await fs.mkdir(sessionDir, { recursive: true })
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type RuntimeConfig = {
|
|||||||
keepAlive: boolean | null
|
keepAlive: boolean | null
|
||||||
prewarm: boolean | null
|
prewarm: boolean | null
|
||||||
slowMoMs: number | null
|
slowMoMs: number | null
|
||||||
|
vncResolution: string
|
||||||
}
|
}
|
||||||
session: {
|
session: {
|
||||||
debug: boolean
|
debug: boolean
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ COPY --from=builder /app/node_modules ./node_modules
|
|||||||
# Compiled application
|
# Compiled application
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json ./package.json
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/config ./config
|
|
||||||
# Playwright browsers
|
# Playwright browsers
|
||||||
COPY --from=builder /ms-playwright /ms-playwright
|
COPY --from=builder /ms-playwright /ms-playwright
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ services:
|
|||||||
DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site}
|
DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site}
|
||||||
DATABASE_SSL: ${DATABASE_SSL:-false}
|
DATABASE_SSL: ${DATABASE_SSL:-false}
|
||||||
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-10}
|
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-10}
|
||||||
|
DATA_ROOT: ${DATA_ROOT:-/app/data}
|
||||||
OCR_BASE_URL: ${OCR_BASE_URL:-http://ocr-worker:8100}
|
OCR_BASE_URL: ${OCR_BASE_URL:-http://ocr-worker:8100}
|
||||||
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
||||||
TENCENT_REDEEM_PROOF_MODE: ${TENCENT_REDEEM_PROOF_MODE:-full}
|
TENCENT_REDEEM_PROOF_MODE: ${TENCENT_REDEEM_PROOF_MODE:-full}
|
||||||
@@ -79,7 +80,6 @@ services:
|
|||||||
ADMIN_DEFAULT_USERS_JSON: ${ADMIN_DEFAULT_USERS_JSON}
|
ADMIN_DEFAULT_USERS_JSON: ${ADMIN_DEFAULT_USERS_JSON}
|
||||||
AGISO_APP_SECRET: ${AGISO_APP_SECRET:-}
|
AGISO_APP_SECRET: ${AGISO_APP_SECRET:-}
|
||||||
AGISO_MESSAGING_ENABLED: ${AGISO_MESSAGING_ENABLED:-false}
|
AGISO_MESSAGING_ENABLED: ${AGISO_MESSAGING_ENABLED:-false}
|
||||||
AGISO_MESSAGE_APP_SECRET: ${AGISO_MESSAGE_APP_SECRET:-}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./apps/backend:/app
|
- ./apps/backend:/app
|
||||||
- backend_node_modules:/app/node_modules
|
- backend_node_modules:/app/node_modules
|
||||||
|
|||||||
+1
-1
@@ -56,6 +56,7 @@ services:
|
|||||||
DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site}
|
DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site}
|
||||||
DATABASE_SSL: ${DATABASE_SSL:-false}
|
DATABASE_SSL: ${DATABASE_SSL:-false}
|
||||||
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-20}
|
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-20}
|
||||||
|
DATA_ROOT: ${DATA_ROOT:-/app/data}
|
||||||
OCR_BASE_URL: ${OCR_BASE_URL:-http://ocr-worker:8100}
|
OCR_BASE_URL: ${OCR_BASE_URL:-http://ocr-worker:8100}
|
||||||
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
||||||
TENCENT_REDEEM_PROOF_MODE: ${TENCENT_REDEEM_PROOF_MODE:-full}
|
TENCENT_REDEEM_PROOF_MODE: ${TENCENT_REDEEM_PROOF_MODE:-full}
|
||||||
@@ -67,7 +68,6 @@ services:
|
|||||||
ADMIN_DEFAULT_USERS_JSON: ${ADMIN_DEFAULT_USERS_JSON}
|
ADMIN_DEFAULT_USERS_JSON: ${ADMIN_DEFAULT_USERS_JSON}
|
||||||
AGISO_APP_SECRET: ${AGISO_APP_SECRET:-}
|
AGISO_APP_SECRET: ${AGISO_APP_SECRET:-}
|
||||||
AGISO_MESSAGING_ENABLED: ${AGISO_MESSAGING_ENABLED:-false}
|
AGISO_MESSAGING_ENABLED: ${AGISO_MESSAGING_ENABLED:-false}
|
||||||
AGISO_MESSAGE_APP_SECRET: ${AGISO_MESSAGE_APP_SECRET:-}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./deploy/data/backend:/app/data
|
- ./deploy/data/backend:/app/data
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -288,7 +288,7 @@ Node 侧实现等价为:
|
|||||||
|
|
||||||
需要修改:
|
需要修改:
|
||||||
|
|
||||||
- `apps/backend/config/default.cjs`
|
- `apps/backend/src/config/defaults.ts`
|
||||||
- `apps/backend/src/config/runtime.js`
|
- `apps/backend/src/config/runtime.js`
|
||||||
- `apps/backend/src/types/runtime-config.js`
|
- `apps/backend/src/types/runtime-config.js`
|
||||||
- `apps/backend/src/index.js`
|
- `apps/backend/src/index.js`
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
|
|
||||||
1. 新增 `tsconfig.build.json`
|
1. 新增 `tsconfig.build.json`
|
||||||
2. `npm run build` 输出 `dist`,并复制数据库 migration SQL
|
2. `npm run build` 输出 `dist`,并复制数据库 migration SQL
|
||||||
3. `npm run dev` 改为 `tsx watch --clear-screen=false --include config src/index.ts`
|
3. `npm run dev` 改为 `tsx watch --clear-screen=false src/index.ts`
|
||||||
4. `npm test` 改为 `node --import tsx --test ...`
|
4. `npm test` 改为 `node --import tsx --test ...`
|
||||||
5. `npm run db:migrate` 改为 `tsx src/db/migrate.ts`
|
5. `npm run db:migrate` 改为 `tsx src/db/migrate.ts`
|
||||||
6. `npm run start` 改为 `node dist/index.js`
|
6. `npm run start` 改为 `node dist/index.js`
|
||||||
@@ -240,7 +240,6 @@
|
|||||||
20. `runtime.js` 已拆分并迁移到 `.ts`:
|
20. `runtime.js` 已拆分并迁移到 `.ts`:
|
||||||
- `src/config/runtime.ts`
|
- `src/config/runtime.ts`
|
||||||
- `src/config/runtime-env.ts`
|
- `src/config/runtime-env.ts`
|
||||||
- `src/config/runtime-loader.ts`
|
|
||||||
- `src/config/runtime-merge.ts`
|
- `src/config/runtime-merge.ts`
|
||||||
21. env 文件加载、env 值解析、默认配置加载、深合并逻辑已从 runtime 主入口分离
|
21. env 文件加载、env 值解析、默认配置加载、深合并逻辑已从 runtime 主入口分离
|
||||||
22. Docker 内再次验证通过:
|
22. Docker 内再次验证通过:
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ MD5(password).toString(Hex)
|
|||||||
|
|
||||||
建议在:
|
建议在:
|
||||||
|
|
||||||
- `apps/backend/config/default.cjs`
|
- `apps/backend/src/config/defaults.ts`
|
||||||
- `apps/backend/src/types/runtime-config.js`
|
- `apps/backend/src/types/runtime-config.js`
|
||||||
- `apps/backend/src/config/runtime.js`
|
- `apps/backend/src/config/runtime.js`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user