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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui-frpc</title>
<script type="module" crossorigin src="/assets/index-Db-ysDeU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D_HiVU-N.css">
<script type="module" crossorigin src="/assets/index-D5XN_p_5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B38pSFA7.css">
</head>
<body>
<div id="app"></div>

View File

@ -24,6 +24,14 @@ type Proxy struct {
SubDomain string
// CustomDomains is used by http/https proxies when set.
CustomDomains []string
// Advanced transport knobs (M1): proxy-level encryption/compression/limit.
UseEncryption bool
UseCompression bool
BandwidthLimit string
PoolCount int
Metadatas map[string]string
Annotations map[string]string
}
// frpcAuth mirrors frpc's auth section.
@ -35,6 +43,14 @@ type frpcAuth struct {
// frpcTransport is the transport section used by frpc.
type frpcTransport struct {
Protocol string `json:"protocol,omitempty"`
TLS *frpcTLS `json:"tls,omitempty"`
PoolCount int `json:"poolCount,omitempty"`
}
// frpcTLS mirrors frpc's transport.tls section.
type frpcTLS struct {
Enable bool `json:"enable,omitempty"`
ServerName string `json:"serverName,omitempty"`
}
// frpcProxy is the serialized proxy object. Different types use different
@ -47,6 +63,14 @@ type frpcProxy struct {
RemotePort int `json:"remotePort,omitempty"`
CustomDomains []string `json:"customDomains,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
// M1 advanced transport fields.
UseEncryption bool `json:"useEncryption,omitempty"`
UseCompression bool `json:"useCompression,omitempty"`
BandwidthLimit string `json:"bandwidthLimit,omitempty"`
PoolCount int `json:"poolCount,omitempty"`
Metadatas map[string]string `json:"metadatas,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}
// Config is a complete frpc configuration document.
@ -69,6 +93,18 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
LoginFailExit: false,
Proxies: make([]frpcProxy, 0, len(proxies)),
}
if remote.TransportProtocol != "" || remote.TransportTLS || remote.TransportPool > 0 || remote.TransportTLSServerName != "" {
cfg.Transport = frpcTransport{
Protocol: remote.TransportProtocol,
PoolCount: remote.TransportPool,
}
if remote.TransportTLS || remote.TransportTLSServerName != "" {
cfg.Transport.TLS = &frpcTLS{
Enable: remote.TransportTLS,
ServerName: remote.TransportTLSServerName,
}
}
}
nameCounts := make(map[string]int)
for _, p := range proxies {
@ -78,6 +114,12 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
LocalIP: p.LocalIP,
LocalPort: p.LocalPort,
SubDomain: p.SubDomain,
UseEncryption: p.UseEncryption,
UseCompression: p.UseCompression,
BandwidthLimit: p.BandwidthLimit,
PoolCount: p.PoolCount,
Metadatas: p.Metadatas,
Annotations: p.Annotations,
}
// http/https proxies use customDomains instead of remotePort. The
// default domain is <name>.local; frps matches it by Host header.

View File

@ -46,3 +46,58 @@ func TestRenderDuplicateServiceGetsPortSuffix(t *testing.T) {
t.Fatalf("second proxy name = %q", cfg.Proxies[1].Name)
}
}
func TestRenderTransportAdvanced(t *testing.T) {
remote := store.Remote{
Name: "srv", IP: "1.2.3.4", Port: 7000, Token: "secret",
TransportProtocol: "kcp", TransportTLS: true, TransportPool: 3, TransportTLSServerName: "frps.local",
}
data, err := Render(remote, []Proxy{
{Name: "web", Type: "tcp", LocalIP: "127.0.0.1", LocalPort: 8080, RemotePort: 8080,
UseEncryption: true, UseCompression: true, BandwidthLimit: "1MB", PoolCount: 2,
Metadatas: map[string]string{"env": "prod"}, Annotations: map[string]string{"owner": "ops"}},
})
if err != nil {
t.Fatal(err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Transport.Protocol != "kcp" {
t.Fatalf("transport.protocol = %q, want kcp", cfg.Transport.Protocol)
}
if cfg.Transport.TLS == nil || !cfg.Transport.TLS.Enable || cfg.Transport.TLS.ServerName != "frps.local" {
t.Fatalf("transport.tls = %+v", cfg.Transport.TLS)
}
if cfg.Transport.PoolCount != 3 {
t.Fatalf("transport.poolCount = %d, want 3", cfg.Transport.PoolCount)
}
p := cfg.Proxies[0]
if !p.UseEncryption || !p.UseCompression || p.BandwidthLimit != "1MB" || p.PoolCount != 2 {
t.Fatalf("proxy advanced fields = %+v", p)
}
if p.Metadatas["env"] != "prod" || p.Annotations["owner"] != "ops" {
t.Fatalf("proxy metadatas/annotations = %+v", p)
}
}
func TestRenderTransportOmittedWhenEmpty(t *testing.T) {
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000}
data, err := Render(remote, []Proxy{
{Name: "web", Type: "tcp", LocalIP: "127.0.0.1", LocalPort: 8080, RemotePort: 8080},
})
if err != nil {
t.Fatal(err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Transport.Protocol != "" || cfg.Transport.TLS != nil || cfg.Transport.PoolCount != 0 {
t.Fatalf("transport should be empty, got %+v", cfg.Transport)
}
if cfg.Proxies[0].UseEncryption || cfg.Proxies[0].BandwidthLimit != "" {
t.Fatalf("proxy advanced should be empty, got %+v", cfg.Proxies[0])
}
}

View File

@ -8,6 +8,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
_ "modernc.org/sqlite"
)
@ -18,6 +19,14 @@ type Local struct {
IP string `json:"ip"`
Port int `json:"port"`
Protocol string `json:"protocol"` // tcp | udp | http | https
// Advanced transport knobs (M1). All optional; empty/zero = frpc defaults.
UseEncryption bool `json:"useEncryption,omitempty"`
UseCompression bool `json:"useCompression,omitempty"`
BandwidthLimit string `json:"bandwidthLimit,omitempty"` // e.g. "1MB", "2KB"
PoolCount int `json:"poolCount,omitempty"`
Metadatas map[string]string `json:"metadatas,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}
// Remote is a remote server node on the canvas.
@ -28,6 +37,12 @@ type Remote struct {
Token string `json:"token,omitempty"`
URL string `json:"url,omitempty"`
Enabled bool `json:"enabled"`
// Transport section (M1). Protocol: tcp | quic | kcp | websocket.
TransportProtocol string `json:"transportProtocol,omitempty"`
TransportTLS bool `json:"transportTls,omitempty"`
TransportPool int `json:"transportPool,omitempty"`
TransportTLSServerName string `json:"transportTlsServerName,omitempty"`
}
// Link connects one local to one remote.
@ -110,11 +125,65 @@ func New(path string) (*Store, error) {
db.Close()
return nil, fmt.Errorf("enable fk: %w", err)
}
st := &Store{db: db}
if err := st.migrate(); err != nil {
db.Close()
return nil, fmt.Errorf("migrate schema: %w", err)
}
if err := os.Chmod(path, 0o600); err != nil {
db.Close()
return nil, fmt.Errorf("secure db: %w", err)
}
return &Store{db: db}, nil
return st, nil
}
// migrate adds columns introduced after the initial schema so old databases
// keep working (CREATE TABLE IF NOT EXISTS does not touch existing tables).
func (s *Store) migrate() error {
tables := map[string][]string{
"locals": {
"use_encryption INTEGER NOT NULL DEFAULT 0",
"use_compression INTEGER NOT NULL DEFAULT 0",
"bandwidth_limit TEXT NOT NULL DEFAULT ''",
"pool_count INTEGER NOT NULL DEFAULT 0",
"metadatas TEXT NOT NULL DEFAULT ''",
"annotations TEXT NOT NULL DEFAULT ''",
},
"remotes": {
"transport_protocol TEXT NOT NULL DEFAULT ''",
"transport_tls INTEGER NOT NULL DEFAULT 0",
"transport_pool INTEGER NOT NULL DEFAULT 0",
"transport_tls_server_name TEXT NOT NULL DEFAULT ''",
},
}
for table, cols := range tables {
rows, err := s.db.Query("PRAGMA table_info(" + table + ")")
if err != nil {
return err
}
have := map[string]bool{}
for rows.Next() {
var cid int
var name, typ string
var notnull, pk int
var dflt sql.NullString
if err := rows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk); err != nil {
rows.Close()
return err
}
have[name] = true
}
rows.Close()
for _, def := range cols {
name := def[:strings.Index(def, " ")]
if !have[name] {
if _, err := s.db.Exec("ALTER TABLE " + table + " ADD COLUMN " + def); err != nil {
return fmt.Errorf("migrate %s.%s: %w", table, name, err)
}
}
}
}
return nil
}
func (s *Store) Close() error { return s.db.Close() }
@ -122,7 +191,7 @@ func (s *Store) Close() error { return s.db.Close() }
// ---- Locals ----
func (s *Store) ListLocals() ([]Local, error) {
rows, err := s.db.Query("SELECT name, ip, port, protocol FROM locals ORDER BY name")
rows, err := s.db.Query("SELECT name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations FROM locals ORDER BY name")
if err != nil {
return nil, err
}
@ -130,9 +199,15 @@ func (s *Store) ListLocals() ([]Local, error) {
var out []Local
for rows.Next() {
var l Local
if err := rows.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol); err != nil {
var enc, comp int
var metaRaw, annoRaw string
if err := rows.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol, &enc, &comp, &l.BandwidthLimit, &l.PoolCount, &metaRaw, &annoRaw); err != nil {
return nil, err
}
l.UseEncryption = enc != 0
l.UseCompression = comp != 0
l.Metadatas = decodeMap(metaRaw)
l.Annotations = decodeMap(annoRaw)
out = append(out, l)
}
return out, rows.Err()
@ -140,22 +215,52 @@ func (s *Store) ListLocals() ([]Local, error) {
func (s *Store) GetLocal(name string) (Local, bool) {
var l Local
row := s.db.QueryRow("SELECT name, ip, port, protocol FROM locals WHERE name = ?", name)
if err := row.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol); err != nil {
var enc, comp int
var metaRaw, annoRaw string
row := s.db.QueryRow("SELECT name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations FROM locals WHERE name = ?", name)
if err := row.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol, &enc, &comp, &l.BandwidthLimit, &l.PoolCount, &metaRaw, &annoRaw); err != nil {
return Local{}, false
}
l.UseEncryption = enc != 0
l.UseCompression = comp != 0
l.Metadatas = decodeMap(metaRaw)
l.Annotations = decodeMap(annoRaw)
return l, true
}
func (s *Store) UpsertLocal(l Local) error {
_, err := s.db.Exec(
"INSERT INTO locals(name, ip, port, protocol) VALUES(?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, protocol=excluded.protocol",
l.Name, l.IP, l.Port, l.Protocol,
"INSERT INTO locals(name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations) VALUES(?,?,?,?,?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, protocol=excluded.protocol, use_encryption=excluded.use_encryption, use_compression=excluded.use_compression, bandwidth_limit=excluded.bandwidth_limit, pool_count=excluded.pool_count, metadatas=excluded.metadatas, annotations=excluded.annotations",
l.Name, l.IP, l.Port, l.Protocol, boolToInt(l.UseEncryption), boolToInt(l.UseCompression), l.BandwidthLimit, l.PoolCount, encodeMap(l.Metadatas), encodeMap(l.Annotations),
)
return err
}
// encodeMap serializes an optional JSON map so nil stays an empty string.
func encodeMap(m map[string]string) string {
if len(m) == 0 {
return ""
}
b, err := json.Marshal(m)
if err != nil {
return ""
}
return string(b)
}
// decodeMap reads a JSON map column; empty/invalid becomes nil.
func decodeMap(raw string) map[string]string {
if raw == "" {
return nil
}
var m map[string]string
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return nil
}
return m
}
func (s *Store) DeleteLocal(name string) error {
_, err := s.db.Exec("DELETE FROM locals WHERE name = ?", name)
return err
@ -164,7 +269,7 @@ func (s *Store) DeleteLocal(name string) error {
// ---- Remotes ----
func (s *Store) ListRemotes() ([]Remote, error) {
rows, err := s.db.Query("SELECT name, ip, port, token, url, enabled FROM remotes ORDER BY name")
rows, err := s.db.Query("SELECT name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name FROM remotes ORDER BY name")
if err != nil {
return nil, err
}
@ -172,11 +277,12 @@ func (s *Store) ListRemotes() ([]Remote, error) {
var out []Remote
for rows.Next() {
var r Remote
var en int
if err := rows.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en); err != nil {
var en, tls int
if err := rows.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en, &r.TransportProtocol, &tls, &r.TransportPool, &r.TransportTLSServerName); err != nil {
return nil, err
}
r.Enabled = en != 0
r.TransportTLS = tls != 0
out = append(out, r)
}
return out, rows.Err()
@ -184,20 +290,21 @@ func (s *Store) ListRemotes() ([]Remote, error) {
func (s *Store) GetRemote(name string) (Remote, bool) {
var r Remote
var en int
row := s.db.QueryRow("SELECT name, ip, port, token, url, enabled FROM remotes WHERE name = ?", name)
if err := row.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en); err != nil {
var en, tls int
row := s.db.QueryRow("SELECT name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name FROM remotes WHERE name = ?", name)
if err := row.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en, &r.TransportProtocol, &tls, &r.TransportPool, &r.TransportTLSServerName); err != nil {
return Remote{}, false
}
r.Enabled = en != 0
r.TransportTLS = tls != 0
return r, true
}
func (s *Store) UpsertRemote(r Remote) error {
_, err := s.db.Exec(
"INSERT INTO remotes(name, ip, port, token, url, enabled) VALUES(?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, token=excluded.token, url=excluded.url, enabled=excluded.enabled",
r.Name, r.IP, r.Port, r.Token, r.URL, boolToInt(r.Enabled),
"INSERT INTO remotes(name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name) VALUES(?,?,?,?,?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, token=excluded.token, url=excluded.url, enabled=excluded.enabled, transport_protocol=excluded.transport_protocol, transport_tls=excluded.transport_tls, transport_pool=excluded.transport_pool, transport_tls_server_name=excluded.transport_tls_server_name",
r.Name, r.IP, r.Port, r.Token, r.URL, boolToInt(r.Enabled), r.TransportProtocol, boolToInt(r.TransportTLS), r.TransportPool, r.TransportTLSServerName,
)
return err
}

View File

@ -41,10 +41,10 @@
目标:让常用 frpc 能力可配置,保持画布简洁(折叠高级区)。
- [ ] Local 增加高级字段useEncryption / useCompression / bandwidthLimit / poolCount / metadatas / annotations渲染器 + 编辑表单 + 折叠 UI
- [ ] Remote 增加 transportprotocoltcp/quic/kcp/websocket、tls.enable、poolCount
- [ ] 渲染器补齐 transport 段输出;增加 render 单测覆盖新字段
- [ ] 高级字段随 canvas API 持久化store 表扩展 + 迁移)
- [x] Local 增加高级字段useEncryption / useCompression / bandwidthLimit / poolCount / metadatas / annotations渲染器 + 编辑表单 + 折叠 UI
- [x] Remote 增加 transportprotocoltcp/quic/kcp/websocket、tls.enable、poolCount
- [x] 渲染器补齐 transport 段输出;增加 render 单测覆盖新字段
- [x] 高级字段随 canvas API 持久化store 表扩展 + 迁移)
### M2 HTTP/HTTPS 完善

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 {

View File

@ -1,17 +1,17 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url";
// Backend default port (cmd/webui4frpc -addr). Override with VITE_PROXY_TARGET.
const backend = process.env.VITE_PROXY_TARGET || 'http://127.0.0.1:7500'
const backend = process.env.VITE_PROXY_TARGET || "http://127.0.0.1:7500";
// The frontend is served by the Go binary at / (embedded dist), and talks to
// the same origin via /api/manager/*.
export default defineConfig({
plugins: [vue()],
base: '/',
base: "/",
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
},
css: {
preprocessorOptions: {
@ -21,11 +21,11 @@ export default defineConfig({
},
},
build: {
outDir: 'dist',
outDir: "dist",
},
server: {
proxy: {
'/api': backend,
"/api": backend,
},
},
})
});