优化配置文件

This commit is contained in:
yml
2026-05-21 19:29:00 +08:00
parent 589de4b50b
commit 7513958a4c
23 changed files with 1061 additions and 676 deletions
+6 -5
View File
@@ -9,8 +9,8 @@ CLAIM_TOKEN_TTL_HOURS=24
POSTGRES_DB=order_site
POSTGRES_USER=postgres
POSTGRES_PASSWORD=FACCqheAzy51op0RFdbv
DATABASE_URL=postgres://postgres:FACCqheAzy51op0RFdbv@postgres:5432/order_site
POSTGRES_PASSWORD=change-me-postgres-password
DATABASE_URL=postgres://postgres:change-me-postgres-password@postgres:5432/order_site
DATABASE_SSL=false
DATABASE_MAX_CONNECTIONS=20
@@ -24,6 +24,7 @@ TENCENT_BROWSER_DEVTOOLS=false
TENCENT_BROWSER_PREWARM=true
TENCENT_BROWSER_KEEP_ALIVE=true
TENCENT_BROWSER_SLOW_MO=0
TENCENT_BROWSER_VNC_RESOLUTION=1600x900x24
TENCENT_SESSION_DEBUG=false
CLOUDTENTACLES_BIND_URL_TTL_SECONDS=600
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_REFERER=https://gp.qq.com/
ADMIN_SESSION_SECRET=xm4BDpbdHJvU4RIlpGqD0nvQTZtIidvQMk6LSP1d
ADMIN_SESSION_SECRET=change-me-long-random-session-secret
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_TRADE_DETAIL_ENDPOINT=https://gw-api.agiso.com/aldsIdle/Order/Detail
AGISO_TRADE_DETAIL_API_VERSION=1
+2 -2
View File
@@ -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`
当前最常改的配置有:
-131
View File
@@ -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',
},
}
+1 -1
View File
@@ -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",
+136
View File
@@ -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',
},
}
}
+178
View File
@@ -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
}
-15
View File
@@ -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')}`
}
+4 -506
View File
@@ -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));
+8
View File
@@ -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()
+1
View File
@@ -29,6 +29,7 @@ export type RuntimeConfig = {
keepAlive: boolean | null
prewarm: boolean | null
slowMoMs: number | null
vncResolution: string
}
session: {
debug: boolean
-1
View File
@@ -90,7 +90,6 @@ COPY --from=builder /app/node_modules ./node_modules
# Compiled application
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/config ./config
# Playwright browsers
COPY --from=builder /ms-playwright /ms-playwright
+1 -1
View File
@@ -62,6 +62,7 @@ services:
DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site}
DATABASE_SSL: ${DATABASE_SSL:-false}
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-10}
DATA_ROOT: ${DATA_ROOT:-/app/data}
OCR_BASE_URL: ${OCR_BASE_URL:-http://ocr-worker:8100}
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
TENCENT_REDEEM_PROOF_MODE: ${TENCENT_REDEEM_PROOF_MODE:-full}
@@ -79,7 +80,6 @@ services:
ADMIN_DEFAULT_USERS_JSON: ${ADMIN_DEFAULT_USERS_JSON}
AGISO_APP_SECRET: ${AGISO_APP_SECRET:-}
AGISO_MESSAGING_ENABLED: ${AGISO_MESSAGING_ENABLED:-false}
AGISO_MESSAGE_APP_SECRET: ${AGISO_MESSAGE_APP_SECRET:-}
volumes:
- ./apps/backend:/app
- backend_node_modules:/app/node_modules
+1 -1
View File
@@ -56,6 +56,7 @@ services:
DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site}
DATABASE_SSL: ${DATABASE_SSL:-false}
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-20}
DATA_ROOT: ${DATA_ROOT:-/app/data}
OCR_BASE_URL: ${OCR_BASE_URL:-http://ocr-worker:8100}
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
TENCENT_REDEEM_PROOF_MODE: ${TENCENT_REDEEM_PROOF_MODE:-full}
@@ -67,7 +68,6 @@ services:
ADMIN_DEFAULT_USERS_JSON: ${ADMIN_DEFAULT_USERS_JSON}
AGISO_APP_SECRET: ${AGISO_APP_SECRET:-}
AGISO_MESSAGING_ENABLED: ${AGISO_MESSAGING_ENABLED:-false}
AGISO_MESSAGE_APP_SECRET: ${AGISO_MESSAGE_APP_SECRET:-}
volumes:
- ./deploy/data/backend:/app/data
+1 -1
View File
@@ -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/types/runtime-config.js`
- `apps/backend/src/index.js`
+1 -2
View File
@@ -199,7 +199,7 @@
1. 新增 `tsconfig.build.json`
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 ...`
5. `npm run db:migrate` 改为 `tsx src/db/migrate.ts`
6. `npm run start` 改为 `node dist/index.js`
@@ -240,7 +240,6 @@
20. `runtime.js` 已拆分并迁移到 `.ts`
- `src/config/runtime.ts`
- `src/config/runtime-env.ts`
- `src/config/runtime-loader.ts`
- `src/config/runtime-merge.ts`
21. env 文件加载、env 值解析、默认配置加载、深合并逻辑已从 runtime 主入口分离
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/config/runtime.js`