feat: M1 高级传输参数(P0)

- store: Local 增加 useEncryption/useCompression/bandwidthLimit/poolCount/metadatas/annotations;Remote 增加 transportProtocol/transportTls/transportPool/transportTlsServerName;新增 migrate() ALTER TABLE 迁移旧库

- render: 输出 transport 段(protocol/tls/poolCount)与 proxy 高级字段;补 TestRenderTransportAdvanced/OmittedWhenEmpty

- main: renderRemote 闭包透传 M1 字段到 render.Proxy

- web: LocalNode/RemoteNode 折叠高级区表单,types.ts 接口扩展

- 验证:go test 全绿、vue-tsc/build 通过、端到端 PUT canvas→frpc JSON 输出完整
This commit is contained in:
2026-08-17 12:28:54 +08:00
parent e73bd834a1
commit 07f25ea067
11 changed files with 460 additions and 62 deletions

View File

@ -28,12 +28,32 @@
</select>
</label>
</div>
<div class="adv">
<button class="adv-toggle" @click.stop="showAdv = !showAdv">
{{ showAdv ? ' 收起高级' : ' 高级' }}
</button>
<div v-if="showAdv" class="adv-body">
<label class="adv-check">
<input v-model="useEncryptionField" type="checkbox" /> 加密
(useEncryption)
</label>
<label class="adv-check">
<input v-model="useCompressionField" type="checkbox" /> 压缩
(useCompression)
</label>
<label>限速 <input v-model="bandwidthLimitField" placeholder="如 1MB" /></label>
<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>
</div>
</div>
</template>
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import type { CanvasLocal } from '../types'
const props = defineProps<{
@ -59,6 +79,41 @@ const nameField = field('name')
const ipField = field('ip')
const portField = field('port')
const protocolField = field('protocol')
const useEncryptionField = field('useEncryption')
const useCompressionField = field('useCompression')
const bandwidthLimitField = field('bandwidthLimit')
const poolCountField = field('poolCount')
// map fields shown as JSON strings in the UI
const showAdv = ref(false)
const metadatasField = computed({
get: () => JSON.stringify(props.data.metadatas ?? {}),
set: (v: string) => {
let parsed: Record<string, string> = {}
if (v.trim()) {
try {
parsed = JSON.parse(v)
} catch {
return // keep old value on invalid JSON
}
}
emit('update:data', { ...props.data, metadatas: parsed })
},
})
const annotationsField = computed({
get: () => JSON.stringify(props.data.annotations ?? {}),
set: (v: string) => {
let parsed: Record<string, string> = {}
if (v.trim()) {
try {
parsed = JSON.parse(v)
} catch {
return
}
}
emit('update:data', { ...props.data, annotations: parsed })
},
})
</script>
<style scoped lang="scss">
@ -162,5 +217,54 @@ const protocolField = field('protocol')
flex: 0 0 64px;
}
}
.adv {
margin-top: 6px;
.adv-toggle {
border: 1px dashed #a5d6a7;
background: transparent;
color: #558b2f;
font-size: 11px;
border-radius: 6px;
padding: 2px 8px;
cursor: pointer;
}
.adv-body {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
padding: 6px;
background: rgba(76, 175, 80, 0.06);
border-radius: 8px;
label {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #558b2f;
input,
select {
flex: 1;
min-width: 0;
border: 1px solid #a5d6a7;
border-radius: 6px;
padding: 2px 6px;
font-size: 12px;
background: #fff;
color: #333;
&:focus {
outline: none;
border-color: #4caf50;
}
}
}
.adv-check {
font-size: 11px;
}
}
}
}
</style>

View File

@ -25,12 +25,34 @@
<label>令牌 <input v-model="tokenField" /></label>
<label>URL <input v-model="urlField" /></label>
</div>
<div class="adv">
<button class="adv-toggle" @click.stop="showAdv = !showAdv">
{{ showAdv ? ' 收起高级' : ' 高级' }}
</button>
<div v-if="showAdv" class="adv-body">
<label
>传输协议
<select v-model="transportProtocolField">
<option value="">tcp(默认)</option>
<option value="quic">quic</option>
<option value="kcp">kcp</option>
<option value="websocket">websocket</option>
</select>
</label>
<label class="adv-check">
<input v-model="transportTlsField" type="checkbox" /> TLS 传输
</label>
<label>TLS SNI <input v-model="transportTlsServerNameField" placeholder="frps.local" /></label>
<label>连接池 <input v-model.number="transportPoolField" type="number" min="0" /></label>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import type { CanvasRemote } from '../types'
const props = defineProps<{
@ -58,6 +80,11 @@ const portField = field('port')
const tokenField = field('token')
const urlField = field('url')
const enabledField = field('enabled')
const transportProtocolField = field('transportProtocol')
const transportTlsField = field('transportTls')
const transportTlsServerNameField = field('transportTlsServerName')
const transportPoolField = field('transportPool')
const showAdv = ref(false)
</script>
<style scoped lang="scss">
@ -168,5 +195,54 @@ const enabledField = field('enabled')
flex: 0 0 64px;
}
}
.adv {
margin-top: 6px;
.adv-toggle {
border: 1px dashed #ffcc80;
background: transparent;
color: #ef6c00;
font-size: 11px;
border-radius: 6px;
padding: 2px 8px;
cursor: pointer;
}
.adv-body {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
padding: 6px;
background: rgba(255, 152, 0, 0.06);
border-radius: 8px;
label {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #ef6c00;
input,
select {
flex: 1;
min-width: 0;
border: 1px solid #ffcc80;
border-radius: 6px;
padding: 2px 6px;
font-size: 12px;
background: #fff;
color: #333;
&:focus {
outline: none;
border-color: #ff9800;
}
}
}
.adv-check {
font-size: 11px;
}
}
}
}
</style>

View File

@ -13,6 +13,14 @@ export interface Local {
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>
}
export interface Remote {
@ -22,6 +30,12 @@ export interface Remote {
token?: string
url?: string
enabled: boolean
// M1 transport section
transportProtocol?: string // tcp | quic | kcp | websocket
transportTls?: boolean
transportPool?: number
transportTlsServerName?: string
}
export interface Link {