Skip to content

Commit d44890d

Browse files
committed
feat: add preset config management
1 parent bab438c commit d44890d

5 files changed

Lines changed: 333 additions & 3 deletions

File tree

packages/core/src/process/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { consola } from 'consola'
1313
import { join } from 'pathe'
1414
import { BINARY_NAMES } from '../constants'
1515
import { ErrorCode, FrpBridgeError } from '../errors'
16-
import { commandExists, downloadFile, ensureDir, executeCommand, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml } from '../utils'
16+
import { commandExists, downloadFile, ensureDir, executeCommand, findExistingVersion, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml } from '../utils'
1717

1818
export interface ProcessEvent {
1919
type: 'process:started' | 'process:stopped' | 'process:exited' | 'process:error'
@@ -86,7 +86,8 @@ export class FrpProcessManager extends EventEmitter {
8686
/** Ensure version is fetched and binary path is set */
8787
private async ensureVersion(): Promise<void> {
8888
if (!this.version) {
89-
this.version = this.specifiedVersion || await getLatestVersion()
89+
// 优先使用指定版本,否则查找已有版本,不尝试获取最新版本
90+
this.version = this.specifiedVersion || findExistingVersion(this.workDir) || ''
9091
const binaryName = this.mode === 'client' ? BINARY_NAMES.client : BINARY_NAMES.server
9192
this.binaryPath = join(this.workDir, 'bin', this.version, binaryName)
9293
}

packages/core/src/utils/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { exec as execCallback } from 'node:child_process'
6-
import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
6+
import { createWriteStream, existsSync, mkdirSync, readdirSync } from 'node:fs'
77
import { get as httpGet } from 'node:http'
88
import { get as httpsGet } from 'node:https'
99
import process from 'node:process'
@@ -125,6 +125,26 @@ export function ensureDir(dirPath: string): void {
125125
}
126126
}
127127

128+
/** Find existing FRP version in work directory */
129+
export function findExistingVersion(workDir: string): string | null {
130+
const binDir = `${workDir}/bin`
131+
132+
if (!existsSync(binDir)) {
133+
return null
134+
}
135+
136+
try {
137+
const versions = readdirSync(binDir, { withFileTypes: true })
138+
.filter((d: any) => d.isDirectory())
139+
.map((d: any) => d.name)
140+
141+
return versions.length > 0 ? versions[0] : null
142+
}
143+
catch {
144+
return null
145+
}
146+
}
147+
128148
/** Parse TOML-like config to JSON */
129149
export function parseToml(content: string): Record<string, any> {
130150
const lines = content.split('\n')
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* 配置合并方法
3+
* 将预设配置和用户配置合并成最终的 frp 配置
4+
*/
5+
6+
import type { ProxyConfig } from '@frp-bridge/types'
7+
import type { PresetConfig } from './preset-config'
8+
import { writeFileSync } from 'node:fs'
9+
import { ensureDir } from '@frp-bridge/core'
10+
11+
/**
12+
* 合并预设配置和用户配置,生成最终的 TOML 配置
13+
* @param presetConfig 预设配置
14+
* @param userConfig 用户配置的 TOML 字符串
15+
* @param type 配置类型 'frps' | 'frpc'
16+
* @returns 最终的 TOML 配置字符串
17+
*/
18+
export function mergeConfigs(
19+
presetConfig: PresetConfig,
20+
userConfig: string,
21+
type: 'frps' | 'frpc'
22+
): string {
23+
const typeConfig = presetConfig[type]
24+
25+
if (!typeConfig) {
26+
// 如果没有预设配置,直接返回用户配置
27+
return userConfig
28+
}
29+
30+
// 解析用户配置
31+
const userConfigLines = userConfig.split('\n')
32+
33+
// 提取用户配置中的代理部分
34+
const proxiesSection = extractProxiesSection(userConfigLines)
35+
36+
// 生成最终配置
37+
const finalConfigLines: string[] = []
38+
39+
// 1. 添加预设配置的基础参数
40+
if (type === 'frps') {
41+
const frpsConfig = typeConfig as import('./preset-config').FrpsPresetConfig
42+
43+
if (frpsConfig.bindPort) {
44+
finalConfigLines.push(`bindPort = ${frpsConfig.bindPort}`)
45+
}
46+
if (frpsConfig.vhostHTTPPort) {
47+
finalConfigLines.push(`vhostHTTPPort = ${frpsConfig.vhostHTTPPort}`)
48+
}
49+
if (frpsConfig.dashboardPort) {
50+
finalConfigLines.push('')
51+
finalConfigLines.push('[webServer]')
52+
finalConfigLines.push(`addr = "0.0.0.0"`)
53+
finalConfigLines.push(`port = ${frpsConfig.dashboardPort}`)
54+
if (frpsConfig.dashboardUser) {
55+
finalConfigLines.push(`user = "${frpsConfig.dashboardUser}"`)
56+
}
57+
if (frpsConfig.dashboardPassword) {
58+
finalConfigLines.push(`password = "${frpsConfig.dashboardPassword}"`)
59+
}
60+
}
61+
}
62+
else if (type === 'frpc') {
63+
const frpcConfig = typeConfig as import('./preset-config').FrpcPresetConfig
64+
65+
if (frpcConfig.serverAddr) {
66+
finalConfigLines.push(`serverAddr = "${frpcConfig.serverAddr}"`)
67+
}
68+
if (frpcConfig.serverPort) {
69+
finalConfigLines.push(`serverPort = ${frpcConfig.serverPort}`)
70+
}
71+
if (frpcConfig.authToken) {
72+
finalConfigLines.push(`auth.token = "${frpcConfig.authToken}"`)
73+
}
74+
}
75+
76+
// 2. 添加用户配置中的代理部分
77+
if (proxiesSection) {
78+
finalConfigLines.push('')
79+
finalConfigLines.push(...proxiesSection)
80+
}
81+
82+
return finalConfigLines.join('\n')
83+
}
84+
85+
/**
86+
* 从 tunnels 数组生成并保存 FRP 配置文件
87+
* @param configPath 配置文件路径
88+
* @param tunnels tunnels 数组
89+
* @param presetConfig 预设配置
90+
* @param type 配置类型 'frps' | 'frpc'
91+
*/
92+
export function saveFrpConfigFile(
93+
configPath: string,
94+
tunnels: ProxyConfig[],
95+
presetConfig: PresetConfig,
96+
type: 'frps' | 'frpc'
97+
): void {
98+
// 1. 将 tunnels 转换为 TOML 格式
99+
const userConfig = tunnelsToToml(tunnels)
100+
101+
// 2. 合并预设配置和用户配置
102+
const finalConfig = mergeConfigs(presetConfig, userConfig, type)
103+
104+
// 3. 确保目录存在
105+
const targetDir = configPath.includes('/') || configPath.includes('\\')
106+
? configPath.substring(0, Math.max(configPath.lastIndexOf('/'), configPath.lastIndexOf('\\')))
107+
: '.'
108+
ensureDir(targetDir)
109+
110+
// 4. 写入配置文件
111+
writeFileSync(configPath, finalConfig, 'utf-8')
112+
}
113+
114+
/**
115+
* 将 tunnels 数组转换为 TOML 格式
116+
*/
117+
function tunnelsToToml(tunnels: ProxyConfig[]): string {
118+
if (!tunnels || tunnels.length === 0) {
119+
return ''
120+
}
121+
122+
const lines: string[] = []
123+
124+
for (const tunnel of tunnels) {
125+
lines.push('')
126+
lines.push('[[proxies]]')
127+
for (const [key, value] of Object.entries(tunnel)) {
128+
if (value === undefined || value === null)
129+
continue
130+
if (typeof value === 'string') {
131+
lines.push(`${key} = "${value}"`)
132+
}
133+
else if (typeof value === 'number' || typeof value === 'boolean') {
134+
lines.push(`${key} = ${value}`)
135+
}
136+
else if (typeof value === 'object') {
137+
lines.push(`[${key}]`)
138+
for (const [subKey, subValue] of Object.entries(value)) {
139+
if (typeof subValue === 'string') {
140+
lines.push(`${subKey} = "${subValue}"`)
141+
}
142+
else {
143+
lines.push(`${subKey} = ${subValue}`)
144+
}
145+
}
146+
}
147+
}
148+
}
149+
150+
return lines.join('\n')
151+
}
152+
153+
/**
154+
* 从用户配置中提取代理部分
155+
*/
156+
function extractProxiesSection(lines: string[]): string[] {
157+
const proxiesSection: string[] = []
158+
let inProxies = false
159+
160+
for (const line of lines) {
161+
// 检查是否是代理定义
162+
if (line.trim().startsWith('[[proxies]]')) {
163+
inProxies = true
164+
}
165+
166+
if (inProxies) {
167+
proxiesSection.push(line)
168+
}
169+
}
170+
171+
return proxiesSection
172+
}
173+
174+
/**
175+
* 将配置对象转换为 TOML 格式
176+
*/
177+
export function configToToml(config: Record<string, any>): string {
178+
const lines: string[] = []
179+
180+
for (const [key, value] of Object.entries(config)) {
181+
if (value === undefined || value === null) {
182+
continue
183+
}
184+
185+
if (typeof value === 'string') {
186+
lines.push(`${key} = "${value}"`)
187+
}
188+
else if (typeof value === 'number' || typeof value === 'boolean') {
189+
lines.push(`${key} = ${value}`)
190+
}
191+
else if (typeof value === 'object') {
192+
// 嵌套对象
193+
lines.push(`[${key}]`)
194+
for (const [subKey, subValue] of Object.entries(value)) {
195+
if (typeof subValue === 'string') {
196+
lines.push(`${subKey} = "${subValue}"`)
197+
}
198+
else if (typeof subValue === 'number' || typeof subValue === 'boolean') {
199+
lines.push(`${subKey} = ${subValue}`)
200+
}
201+
}
202+
}
203+
}
204+
205+
return lines.join('\n')
206+
}
207+
208+
/**
209+
* 验证预设配置
210+
*/
211+
export function validatePresetConfig(
212+
config: PresetConfig,
213+
type: 'frps' | 'frpc'
214+
): { valid: boolean, errors: string[] } {
215+
const errors: string[] = []
216+
const typeConfig = config[type]
217+
218+
if (!typeConfig) {
219+
return { valid: true, errors: [] }
220+
}
221+
222+
if (type === 'frps') {
223+
const frpsConfig = typeConfig as import('./preset-config').FrpsPresetConfig
224+
225+
if (frpsConfig.bindPort !== undefined) {
226+
if (frpsConfig.bindPort < 1 || frpsConfig.bindPort > 65535) {
227+
errors.push('bindPort must be between 1-65535')
228+
}
229+
}
230+
231+
if (frpsConfig.vhostHTTPPort !== undefined) {
232+
if (frpsConfig.vhostHTTPPort < 1 || frpsConfig.vhostHTTPPort > 65535) {
233+
errors.push('vhostHTTPPort must be between 1-65535')
234+
}
235+
}
236+
237+
if (frpsConfig.domain !== undefined && !frpsConfig.domain) {
238+
errors.push('domain cannot be empty')
239+
}
240+
}
241+
else if (type === 'frpc') {
242+
const frpcConfig = typeConfig as import('./preset-config').FrpcPresetConfig
243+
244+
if (frpcConfig.serverPort !== undefined) {
245+
if (frpcConfig.serverPort < 1 || frpcConfig.serverPort > 65535) {
246+
errors.push('serverPort must be between 1-65535')
247+
}
248+
}
249+
250+
if (frpcConfig.serverAddr !== undefined && !frpcConfig.serverAddr) {
251+
errors.push('serverAddr cannot be empty')
252+
}
253+
}
254+
255+
return {
256+
valid: errors.length === 0,
257+
errors
258+
}
259+
}

packages/frp-bridge/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
export * from './config-merger'
2+
export * from './preset-config'
13
export { FileSnapshotStorage, FrpBridge, FrpProcessManager } from '@frp-bridge/core'
4+
25
export type { FrpBridgeOptions, FrpProcessManagerOptions, NodeInfo } from '@frp-bridge/core'
36
export * from '@frp-bridge/types'
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* 预设配置定义
3+
* 预设配置是系统级配置,用户通过 frp-web 的特定表单设置,不能直接修改文件
4+
*/
5+
6+
export interface PresetConfig {
7+
// frps 预设配置
8+
frps?: FrpsPresetConfig
9+
10+
// frpc 预设配置
11+
frpc?: FrpcPresetConfig
12+
}
13+
14+
export interface FrpsPresetConfig {
15+
bindPort?: number
16+
vhostHTTPPort?: number
17+
domain?: string
18+
dashboardPort?: number
19+
dashboardUser?: string
20+
dashboardPassword?: string
21+
}
22+
23+
export interface FrpcPresetConfig {
24+
serverAddr?: string
25+
serverPort?: number
26+
authToken?: string
27+
}
28+
29+
/**
30+
* 默认预设配置
31+
*/
32+
export const DEFAULT_PRESET_CONFIG: PresetConfig = {
33+
frps: {
34+
bindPort: 7000,
35+
vhostHTTPPort: 7000,
36+
dashboardPort: 7500,
37+
dashboardUser: 'admin'
38+
},
39+
frpc: {
40+
serverPort: 7000
41+
}
42+
}
43+
44+
/**
45+
* 预设配置的存储键
46+
*/
47+
export const PRESET_CONFIG_KEY = 'frp:preset-config'

0 commit comments

Comments
 (0)