feat: M2 HTTP/HTTPS 完善

- store: Local 增加 customDomains/subdomain/locations/hostHeaderRewrite/httpHeaders/basicAuth;Remote 增加 vhostHttpPort;migrate() 补列;CRUD 读写(JSOm 编解码)

- render: http/https proxy 输出 customDomains/subdomain/locations/hostHeaderRewrite/httpHeaders/basicAuth

- main: renderRemote 透传 M2 字段(customDomains 逗号拆分 splitCSV)

- web: LocalNode 折叠 HTTP 路由高级区;RemoteNode vhostHTTPPort;StatusView 显示 HTTP 访问端口

- 测试: TestRenderHTTPAdvanced / TestRenderHTTPSUsesCustomDomains;端到端 PUT canvas→config 验证通过
This commit is contained in:
2026-08-17 12:59:12 +08:00
parent 07f25ea067
commit 58906c5a81
11 changed files with 323 additions and 116 deletions

View File

@ -46,6 +46,15 @@
<label>连接池 <input v-model.number="poolCountField" type="number" min="0" /></label>
<label>元数据(JSON) <input v-model="metadatasField" placeholder='{"env":"prod"}' /></label>
<label>注解(JSON) <input v-model="annotationsField" placeholder='{"owner":"ops"}' /></label>
<div class="adv-sep">HTTP/HTTPS 路由</div>
<label>域名(逗号分隔) <input v-model="customDomainsField" placeholder="app.example.com" /></label>
<label>子域 <input v-model="subdomainField" placeholder="app (需 frps subdomain_host)" /></label>
<label>路径路由(JSON) <input v-model="locationsField" placeholder='["/api","/admin"]' /></label>
<label>Host 改写 <input v-model="hostHeaderRewriteField" placeholder="backend.internal" /></label>
<label>请求头(JSON) <input v-model="httpHeadersField" placeholder='{"X-Custom":"val"}' /></label>
<label>BasicAuth 用户 <input v-model="basicAuthUserField" placeholder="user" /></label>
<label>BasicAuth 密码 <input v-model="basicAuthPasswordField" type="password" placeholder="pass" /></label>
</div>
</div>
</div>
@ -114,6 +123,44 @@ const annotationsField = computed({
emit('update:data', { ...props.data, annotations: parsed })
},
})
const customDomainsField = field('customDomains')
const subdomainField = field('subdomain')
const hostHeaderRewriteField = field('hostHeaderRewrite')
const basicAuthUserField = field('basicAuthUser')
const basicAuthPasswordField = field('basicAuthPassword')
// locations is an array shown as JSON in the UI
const locationsField = computed({
get: () => JSON.stringify(props.data.locations ?? []),
set: (v: string) => {
let parsed: string[] = []
if (v.trim()) {
try {
const arr = JSON.parse(v)
if (Array.isArray(arr)) parsed = arr.map(String)
else return
} catch {
return
}
}
emit('update:data', { ...props.data, locations: parsed })
},
})
// httpHeaders is a map shown as JSON
const httpHeadersField = computed({
get: () => JSON.stringify(props.data.httpHeaders ?? {}),
set: (v: string) => {
let parsed: Record<string, string> = {}
if (v.trim()) {
try {
parsed = JSON.parse(v)
} catch {
return
}
}
emit('update:data', { ...props.data, httpHeaders: parsed })
},
})
</script>
<style scoped lang="scss">

View File

@ -45,6 +45,8 @@
</label>
<label>TLS SNI <input v-model="transportTlsServerNameField" placeholder="frps.local" /></label>
<label>连接池 <input v-model.number="transportPoolField" type="number" min="0" /></label>
<label class="adv-sep">HTTP 访问端口 (frps vhostHTTPPort)</label>
<label>vhostHTTPPort <input v-model.number="vhostHttpPortField" type="number" min="0" /></label>
</div>
</div>
</div>
@ -84,6 +86,7 @@ const transportProtocolField = field('transportProtocol')
const transportTlsField = field('transportTls')
const transportTlsServerNameField = field('transportTlsServerName')
const transportPoolField = field('transportPool')
const vhostHttpPortField = field('vhostHttpPort')
const showAdv = ref(false)
</script>

View File

@ -2,109 +2,121 @@
// CanvasLocal / CanvasRemote are aliases kept for node components migrated
// from the original prototype; they equal Local / Remote.
export type CanvasLocal = Local
export type CanvasRemote = Remote
export type CanvasLocal = Local;
export type CanvasRemote = Remote;
// CanvasLink mirrors the store.Link JSON shape used by the canvas editor.
export type CanvasLink = Link
export type CanvasLink = Link;
export interface Local {
name: string
ip: string
port: number
protocol: string // tcp | udp | http | https
name: string;
ip: string;
port: number;
protocol: string; // tcp | udp | http | https
// M1 advanced transport knobs (optional; empty/zero = frpc defaults)
useEncryption?: boolean
useCompression?: boolean
bandwidthLimit?: string
poolCount?: number
metadatas?: Record<string, string>
annotations?: Record<string, string>
useEncryption?: boolean;
useCompression?: boolean;
bandwidthLimit?: string;
poolCount?: number;
metadatas?: Record<string, string>;
annotations?: Record<string, string>;
// M2 HTTP/HTTPS routing (http/https only)
customDomains?: string; // comma-separated
subdomain?: string;
locations?: string[];
hostHeaderRewrite?: string;
httpHeaders?: Record<string, string>;
basicAuthUser?: string;
basicAuthPassword?: string;
}
export interface Remote {
name: string
ip: string
port: number // frps connect port
token?: string
url?: string
enabled: boolean
name: string;
ip: string;
port: number; // frps connect port
token?: string;
url?: string;
enabled: boolean;
// M1 transport section
transportProtocol?: string // tcp | quic | kcp | websocket
transportTls?: boolean
transportPool?: number
transportTlsServerName?: string
transportProtocol?: string; // tcp | quic | kcp | websocket
transportTls?: boolean;
transportPool?: number;
transportTlsServerName?: string;
// M2: frps vhostHTTPPort
vhostHttpPort?: number;
}
export interface Link {
id?: number
local: string
remote: string
remotePort: number
offsetX?: number
offsetY?: number
id?: number;
local: string;
remote: string;
remotePort: number;
offsetX?: number;
offsetY?: number;
}
export interface CanvasData {
locals: Local[]
remotes: Remote[]
links: Link[]
locals: Local[];
remotes: Remote[];
links: Link[];
}
export interface Settings {
autoStartProfiles: boolean
restartOnExit: boolean
restartIntervalSeconds: number
binaryPath?: string
autoStartProfiles: boolean;
restartOnExit: boolean;
restartIntervalSeconds: number;
binaryPath?: string;
}
export interface ProcessStatus {
state: string
pid?: number
startTime?: number
restartCount: number
exitCode?: number
err?: string
state: string;
pid?: number;
startTime?: number;
restartCount: number;
exitCode?: number;
err?: string;
}
export interface StatusProfile {
name: string
enabled: boolean
process: ProcessStatus
hasProcess: boolean
forwards: { service: string; remotePort: number; localPort?: number }[]
name: string;
enabled: boolean;
process: ProcessStatus;
hasProcess: boolean;
forwards: { service: string; remotePort: number; localPort?: number }[];
}
export interface StatusResp {
version: string
workDir: string
settings: Settings
services: Local[]
remotes: Remote[]
binaryPath: string
profiles: StatusProfile[]
localStatus: LocalStatus[]
version: string;
workDir: string;
settings: Settings;
services: Local[];
remotes: Remote[];
binaryPath: string;
profiles: StatusProfile[];
localStatus: LocalStatus[];
}
export interface LocalTargetStatus {
remote: string
remotePort: number
workerState: string
remote: string;
remotePort: number;
workerState: string;
}
export interface LocalStatus {
local: Local
targets: LocalTargetStatus[]
local: Local;
targets: LocalTargetStatus[];
}
export interface BinaryStatus {
binaryPath: string
version?: string
binaryPath: string;
version?: string;
}
export interface InstallResult {
path: string
version: string
path: string;
version: string;
}

View File

@ -33,6 +33,9 @@
<div class="ns-meta">
<span v-if="p.process.pid">pid {{ p.process.pid }}</span>
<span class="ns-err" v-else-if="p.process.err">{{ p.process.err }}</span>
<span v-if="remoteVhostPort(p.name)" class="ns-vhost">
HTTP 访问端口 :{{ remoteVhostPort(p.name) }}
</span>
</div>
<div class="ns-forwards" v-if="p.forwards.length">
<span v-for="f in p.forwards" :key="f.service" class="ns-fwd">
@ -218,6 +221,10 @@ const workerStateLabel = (s?: string) => {
const workerHealthy = (s?: string) => s === 'running'
// remoteVhostPort returns the frps vhostHTTPPort for a remote (0 if unset).
const remoteVhostPort = (name: string): number =>
status.value?.remotes.find((r) => r.name === name)?.vhostHttpPort || 0
onMounted(() => {
loadStatus()
timer = window.setInterval(loadStatus, 5000)
@ -402,6 +409,15 @@ onBeforeUnmount(() => {
color: $color-danger;
}
.ns-vhost {
margin-left: 8px;
background: #ecf5ff;
color: #409eff;
border-radius: 6px;
padding: 1px 8px;
font-size: 12px;
}
.ns-forwards {
display: flex;
flex-wrap: wrap;