优化配置文件
This commit is contained in:
@@ -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 build` 输出 `dist`,`npm run start` 运行 `dist/index.js`
|
||||
- 新增或迁移后端模块优先使用 `.ts`
|
||||
@@ -53,7 +53,7 @@ npm test
|
||||
|
||||
当前后端采用 `.env` 单一路径做环境配置:
|
||||
|
||||
- 通用默认值:`config/default.cjs`
|
||||
- 通用默认值:`src/config/defaults.ts`
|
||||
- 本地/部署覆盖:工作区根目录 `.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",
|
||||
"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",
|
||||
"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)",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"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 process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { loadEnvFiles, parseBoolean, parseInteger, parseJsonArray } from "./runtime-env.js";
|
||||
import { loadConfig } from "./runtime-loader.js";
|
||||
import { deepMerge } from "./runtime-merge.js";
|
||||
import type { AdminDefaultUser, RuntimeConfig } from "../types/runtime-config.js";
|
||||
import { createDefaultRuntimeConfig } from "./defaults.js";
|
||||
import { applyEnvOverrides } from "./env-overrides.js";
|
||||
import { loadEnvFiles } from "./runtime-env.js";
|
||||
|
||||
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 = loadConfig(path.join(CONFIG_ROOT, "default.cjs")) as RuntimeConfig;
|
||||
|
||||
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;
|
||||
}
|
||||
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT));
|
||||
|
||||
@@ -2,6 +2,7 @@ import process from "node:process";
|
||||
|
||||
import { createApp } from "./app.js";
|
||||
import { runtimeConfig } from "./config/runtime.js";
|
||||
import { assertRuntimeConfigValid } from "./config/runtime-validation.js";
|
||||
import { bootstrapCoreServices } from "./startup/bootstrap.js";
|
||||
import { createShutdownController } from "./startup/shutdown.js";
|
||||
import { createStartupState, formatStartupError } from "./startup/state.js";
|
||||
@@ -13,6 +14,13 @@ const startupState = createStartupState();
|
||||
|
||||
let shutdownController: ReturnType<typeof createShutdownController> | null = null;
|
||||
|
||||
try {
|
||||
assertRuntimeConfigValid(runtimeConfig);
|
||||
} catch (error) {
|
||||
logError("[startup]", "运行时配置校验失败,服务停止启动", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
startupState,
|
||||
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
|
||||
|
||||
@@ -88,7 +88,7 @@ export function buildChromiumLaunchOptions(
|
||||
return options
|
||||
}
|
||||
|
||||
export function resolveDefaultViewport(rawValue = process.env.TENCENT_BROWSER_VNC_RESOLUTION): ViewportSize {
|
||||
export function resolveDefaultViewport(rawValue?: unknown): ViewportSize {
|
||||
const fromVncResolution = parseViewportSpec(rawValue)
|
||||
if (fromVncResolution) {
|
||||
return fromVncResolution
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { resolveDefaultViewport } from './session-browser-config.js'
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..')
|
||||
|
||||
export const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions')
|
||||
export const BROWSER_SESSION_DATA_ROOT = path.join(runtimeConfig.data.root, 'browser-sessions')
|
||||
export const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
|
||||
export const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
|
||||
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)
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import {
|
||||
DATA_ROOT,
|
||||
BROWSER_SESSION_DATA_ROOT,
|
||||
REDEEM_SUCCESS_AUTO_CLOSE_MS,
|
||||
} from './session-service-config.js'
|
||||
import { SessionServiceError } from './session-service-runtime.js'
|
||||
@@ -61,7 +61,7 @@ export async function registerTencentBrowserSession({
|
||||
page,
|
||||
}: RegisterSessionInput): Promise<TencentBrowserSession> {
|
||||
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 })
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
@@ -29,6 +29,7 @@ export type RuntimeConfig = {
|
||||
keepAlive: boolean | null
|
||||
prewarm: boolean | null
|
||||
slowMoMs: number | null
|
||||
vncResolution: string
|
||||
}
|
||||
session: {
|
||||
debug: boolean
|
||||
|
||||
Reference in New Issue
Block a user