Files
openbridge-token-usage-viewer/src/env.ts
MAO Dongyang f2db4bff9d chore: 统一开发服务器端口为 13098
- 更新 .env.example、env.ts、vite.config.ts 默认端口
- 同步更新 sidecar.rs Rust 端口常量
- 更新 README、AGENTS.md 等文档中的端口引用
2026-01-27 11:10:21 +08:00

146 lines
3.5 KiB
TypeScript

/**
* 环境变量配置模块
*
* 使用 @t3-oss/env-core 进行类型安全的环境变量验证。
* 支持从同目录的 .env 文件加载配置(优先级低于系统环境变量)。
*/
import { existsSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod'
/** Token 使用量 API 的默认地址 (按模型分组) */
const DEFAULT_TOKEN_USAGE_URL = 'http://10.0.1.1:8318/api/usage/model'
/** 服务器端口默认值 */
const DEFAULT_SERVER_PORT = '13098'
/**
* 判断当前是否为打包后的可执行文件运行环境
*
* @returns 是否为打包后的二进制文件
*/
const isBundledExec = (): boolean => {
const execPath = process.execPath
return !execPath.includes('node') && !execPath.includes('bun')
}
/**
* 获取配置文件的基础目录
*
* - 打包后的 sidecar: 使用可执行文件所在目录
* - 开发模式: 使用项目根目录
*
* @returns 基础目录路径
*/
const getBaseDir = (): string =>
isBundledExec() ? dirname(process.execPath) : process.cwd()
/**
* 解析 .env 文件内容
*
* 支持:
* - 空行和 # 开头的注释
* - KEY=value 格式
* - 只设置系统环境变量中不存在的变量
*
* @param content - .env 文件内容
* @returns 解析后的环境变量对象
*/
const parseEnvContent = (content: string): Record<string, string> => {
const result: Record<string, string> = {}
for (const line of content.split('\n')) {
const trimmed = line.trim()
// 跳过空行和注释
if (!trimmed || trimmed.startsWith('#')) continue
const eqIndex = trimmed.indexOf('=')
if (eqIndex <= 0) continue
const key = trimmed.slice(0, eqIndex).trim()
const value = trimmed.slice(eqIndex + 1).trim()
// 只设置系统环境变量中不存在的变量
if (!process.env[key]) {
result[key] = value
}
}
return result
}
/**
* 从同目录的 .env 配置文件读取环境变量
*
* 优先级:
* 1. 系统环境变量 (process.env)
* 2. 可执行文件同目录的 .env 文件
* 3. 默认值
*
* @returns 从文件解析的环境变量
*/
const loadEnvFromFile = (): Record<string, string> => {
const envPath = join(getBaseDir(), '.env')
if (!existsSync(envPath)) return {}
try {
const content = readFileSync(envPath, 'utf-8')
return parseEnvContent(content)
} catch {
// 忽略读取错误(权限问题等)
return {}
}
}
/**
* 构建合并后的环境变量对象
*
* 合并顺序: process.env > fileEnv > 默认值
*/
const buildMergedEnv = (): Record<string, string | undefined> => {
const fileEnv = loadEnvFromFile()
const merged: Record<string, string | undefined> = { ...process.env }
// 从文件填充缺失的变量
for (const [key, value] of Object.entries(fileEnv)) {
if (!merged[key]) {
merged[key] = value
}
}
// 设置默认值
merged.TOKEN_USAGE_URL ??= DEFAULT_TOKEN_USAGE_URL
merged.PROJECT_SERVER_PORT ??= DEFAULT_SERVER_PORT
return merged
}
/**
* 类型安全的环境变量配置
*
* 服务端变量:
* - TOKEN_USAGE_URL: Token 使用量 API 地址
*
* 客户端变量 (VITE_ 前缀):
* - VITE_APP_TITLE: 应用标题 (可选)
*/
export const env = createEnv({
server: {
TOKEN_USAGE_URL: z.string().url(),
PROJECT_SERVER_PORT: z.coerce
.number()
.int()
.min(1)
.max(65535)
.default(13098),
},
clientPrefix: 'VITE_',
client: {
VITE_APP_TITLE: z.string().min(1).optional(),
},
runtimeEnv: buildMergedEnv(),
emptyStringAsUndefined: true,
})