diff --git a/.github/update.log b/.github/update.log index cd3014c89f..8cde6690f3 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1216,3 +1216,4 @@ Update On Mon Dec 15 19:43:13 CET 2025 Update On Tue Dec 16 19:42:39 CET 2025 Update On Wed Dec 17 19:43:54 CET 2025 Update On Thu Dec 18 19:42:36 CET 2025 +Update On Fri Dec 19 19:41:31 CET 2025 diff --git a/clash-meta/component/ca/config.go b/clash-meta/component/ca/config.go index 9cc8839f40..f50780af9f 100644 --- a/clash-meta/component/ca/config.go +++ b/clash-meta/component/ca/config.go @@ -10,7 +10,6 @@ import ( "sync" "github.com/metacubex/mihomo/common/once" - C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/tls" @@ -107,12 +106,13 @@ func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) { } if len(opt.Certificate) > 0 || len(opt.PrivateKey) > 0 { - var cert tls.Certificate - cert, err = LoadTLSKeyPair(opt.Certificate, opt.PrivateKey, C.Path) + certLoader, err := NewTLSKeyPairLoader(opt.Certificate, opt.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return certLoader() + } } return tlsConfig, nil } diff --git a/clash-meta/component/ca/keypair.go b/clash-meta/component/ca/keypair.go index 13b38dc11f..ff9086d1b7 100644 --- a/clash-meta/component/ca/keypair.go +++ b/clash-meta/component/ca/keypair.go @@ -12,67 +12,80 @@ import ( "fmt" "math/big" "os" + "runtime" + "sync" "time" + C "github.com/metacubex/mihomo/constant" + + "github.com/metacubex/fswatch" "github.com/metacubex/tls" ) -type Path interface { - Resolve(path string) string - IsSafePath(path string) bool - ErrNotSafePath(path string) error -} - -// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution. -// Returns a tls.Certificate and an error, where the error indicates issues during parsing or file loading. +// NewTLSKeyPairLoader creates a loader function for TLS key pairs from the provided certificate and private key data or file paths. // If both certificate and privateKey are empty, generates a random TLS RSA key pair. -// Accepts a Path interface for resolving file paths when necessary. -func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, error) { +func NewTLSKeyPairLoader(certificate, privateKey string) (func() (*tls.Certificate, error), error) { if certificate == "" && privateKey == "" { var err error certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA) if err != nil { - return tls.Certificate{}, err + return nil, err } } cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) if painTextErr == nil { - return cert, nil - } - if path == nil { - return tls.Certificate{}, painTextErr + return func() (*tls.Certificate, error) { + return &cert, nil + }, nil } - certificate = path.Resolve(certificate) - privateKey = path.Resolve(privateKey) + certificate = C.Path.Resolve(certificate) + privateKey = C.Path.Resolve(privateKey) var loadErr error - if !path.IsSafePath(certificate) { - loadErr = path.ErrNotSafePath(certificate) - } else if !path.IsSafePath(privateKey) { - loadErr = path.ErrNotSafePath(privateKey) + if !C.Path.IsSafePath(certificate) { + loadErr = C.Path.ErrNotSafePath(certificate) + } else if !C.Path.IsSafePath(privateKey) { + loadErr = C.Path.ErrNotSafePath(privateKey) } else { cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) } if loadErr != nil { - return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } - return cert, nil + gcFlag := new(os.File) + updateMutex := sync.RWMutex{} + if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) { + updateMutex.Lock() + defer updateMutex.Unlock() + if newCert, err := tls.LoadX509KeyPair(certificate, privateKey); err == nil { + cert = newCert + } + }}); err == nil { + if err = watcher.Start(); err == nil { + runtime.SetFinalizer(gcFlag, func(f *os.File) { + _ = watcher.Close() + }) + } + } + return func() (*tls.Certificate, error) { + defer runtime.KeepAlive(gcFlag) + updateMutex.RLock() + defer updateMutex.RUnlock() + return &cert, nil + }, nil } -func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) { +func LoadCertificates(certificate string) (*x509.CertPool, error) { pool := x509.NewCertPool() if pool.AppendCertsFromPEM([]byte(certificate)) { return pool, nil } painTextErr := fmt.Errorf("invalid certificate: %s", certificate) - if path == nil { - return nil, painTextErr - } - certificate = path.Resolve(certificate) + certificate = C.Path.Resolve(certificate) var loadErr error - if !path.IsSafePath(certificate) { - loadErr = path.ErrNotSafePath(certificate) + if !C.Path.IsSafePath(certificate) { + loadErr = C.Path.ErrNotSafePath(certificate) } else { certPEMBlock, err := os.ReadFile(certificate) if pool.AppendCertsFromPEM(certPEMBlock) { @@ -83,6 +96,9 @@ func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) { if loadErr != nil { return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } + //TODO: support dynamic update pool too + // blocked by: https://github.com/golang/go/issues/64796 + // maybe we can direct add `GetRootCAs` and `GetClientCAs` to ourselves tls fork return pool, nil } diff --git a/clash-meta/component/ech/key.go b/clash-meta/component/ech/key.go index b0d572d628..4911b90f4c 100644 --- a/clash-meta/component/ech/key.go +++ b/clash-meta/component/ech/key.go @@ -8,9 +8,12 @@ import ( "errors" "fmt" "os" + "runtime" + "sync" - "github.com/metacubex/mihomo/component/ca" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/fswatch" "github.com/metacubex/tls" "golang.org/x/crypto/cryptobyte" ) @@ -104,40 +107,65 @@ func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { return keys, nil } -func LoadECHKey(key string, tlsConfig *tls.Config, path ca.Path) error { +func LoadECHKey(key string, tlsConfig *tls.Config) error { if key == "" { return nil } - painTextErr := loadECHKey([]byte(key), tlsConfig) + echKeys, painTextErr := loadECHKey([]byte(key)) if painTextErr == nil { + tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + return echKeys, nil + } return nil } - key = path.Resolve(key) + key = C.Path.Resolve(key) var loadErr error - if !path.IsSafePath(key) { - loadErr = path.ErrNotSafePath(key) + if !C.Path.IsSafePath(key) { + loadErr = C.Path.ErrNotSafePath(key) } else { var echKey []byte echKey, loadErr = os.ReadFile(key) if loadErr == nil { - loadErr = loadECHKey(echKey, tlsConfig) + echKeys, loadErr = loadECHKey(echKey) } } if loadErr != nil { return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } + gcFlag := new(os.File) + updateMutex := sync.RWMutex{} + if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) { + updateMutex.Lock() + defer updateMutex.Unlock() + if echKey, err := os.ReadFile(key); err == nil { + if newEchKeys, err := loadECHKey(echKey); err == nil { + echKeys = newEchKeys + } + } + }}); err == nil { + if err = watcher.Start(); err == nil { + runtime.SetFinalizer(gcFlag, func(f *os.File) { + _ = watcher.Close() + }) + } + } + tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + defer runtime.KeepAlive(gcFlag) + updateMutex.RLock() + defer updateMutex.RUnlock() + return echKeys, nil + } return nil } -func loadECHKey(echKey []byte, tlsConfig *tls.Config) error { +func loadECHKey(echKey []byte) ([]tls.EncryptedClientHelloKey, error) { block, rest := pem.Decode(echKey) if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { - return errors.New("invalid ECH keys pem") + return nil, errors.New("invalid ECH keys pem") } echKeys, err := UnmarshalECHKeys(block.Bytes) if err != nil { - return fmt.Errorf("parse ECH keys: %w", err) + return nil, fmt.Errorf("parse ECH keys: %w", err) } - tlsConfig.EncryptedClientHelloKeys = echKeys - return nil + return echKeys, err } diff --git a/clash-meta/component/tls/utls.go b/clash-meta/component/tls/utls.go index 2b33d323dd..d8ca9716c5 100644 --- a/clash-meta/component/tls/utls.go +++ b/clash-meta/component/tls/utls.go @@ -1,7 +1,10 @@ package tls import ( + "context" "net" + "reflect" + "unsafe" "github.com/metacubex/mihomo/common/once" "github.com/metacubex/mihomo/common/utils" @@ -126,8 +129,11 @@ type EncryptedClientHelloKey = utls.EncryptedClientHelloKey type Config = utls.Config +var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset +var tlsClientHelloInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.ClientHelloInfo)(nil)).Elem().FieldByName("ctx")).Offset + func UConfig(config *tls.Config) *utls.Config { - return &utls.Config{ + cfg := &utls.Config{ Rand: config.Rand, Time: config.Time, Certificates: utils.Map(config.Certificates, UCertificate), @@ -147,6 +153,52 @@ func UConfig(config *tls.Config) *utls.Config { SessionTicketsDisabled: config.SessionTicketsDisabled, Renegotiation: utls.RenegotiationSupport(config.Renegotiation), } + if config.GetClientCertificate != nil { + cfg.GetClientCertificate = func(info *utls.CertificateRequestInfo) (*utls.Certificate, error) { + tlsInfo := &tls.CertificateRequestInfo{ + AcceptableCAs: info.AcceptableCAs, + SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme { + return tls.SignatureScheme(it) + }), + Version: info.Version, + } + *(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsCertificateRequestInfoCtxOffset)) = info.Context() // for tlsInfo.ctx + cert, err := config.GetClientCertificate(tlsInfo) + if err != nil { + return nil, err + } + uCert := UCertificate(*cert) + return &uCert, err + } + } + if config.GetCertificate != nil { + cfg.GetCertificate = func(info *utls.ClientHelloInfo) (*utls.Certificate, error) { + tlsInfo := &tls.ClientHelloInfo{ + CipherSuites: info.CipherSuites, + ServerName: info.ServerName, + SupportedCurves: utils.Map(info.SupportedCurves, func(it utls.CurveID) tls.CurveID { + return tls.CurveID(it) + }), + SupportedPoints: info.SupportedPoints, + SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme { + return tls.SignatureScheme(it) + }), + SupportedProtos: info.SupportedProtos, + SupportedVersions: info.SupportedVersions, + Extensions: info.Extensions, + Conn: info.Conn, + //HelloRetryRequest: info.HelloRetryRequest, + } + *(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsClientHelloInfoCtxOffset)) = info.Context() // for tlsInfo.ctx + cert, err := config.GetCertificate(tlsInfo) + if err != nil { + return nil, err + } + uCert := UCertificate(*cert) + return &uCert, err + } + } + return cfg } // BuildWebsocketHandshakeState it will only send http/1.1 in its ALPN. diff --git a/clash-meta/config/config.go b/clash-meta/config/config.go index 301126a61e..c2b5664d3d 100644 --- a/clash-meta/config/config.go +++ b/clash-meta/config/config.go @@ -1292,7 +1292,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro } kLower := strings.ToLower(k) if strings.Contains(kLower, ",") { - if strings.Contains(kLower, "geosite:") { + if strings.HasPrefix(kLower, "geosite:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1300,7 +1300,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro newKey := "geosite:" + subkey policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } - } else if strings.Contains(kLower, "rule-set:") { + } else if strings.HasPrefix(kLower, "rule-set:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1315,9 +1315,9 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro } } } else { - if strings.Contains(kLower, "geosite:") { + if strings.HasPrefix(kLower, "geosite:") { policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) - } else if strings.Contains(kLower, "rule-set:") { + } else if strings.HasPrefix(kLower, "rule-set:") { policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) } else { policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) @@ -1712,7 +1712,7 @@ func parseSniffer(snifferRaw RawSniffer, ruleProviders map[string]P.RuleProvider } snifferConfig.SkipSrcAddress = skipSrcAddress - skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-src-address", ruleProviders) + skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-dst-address", ruleProviders) if err != nil { return nil, fmt.Errorf("error in skip-dst-address, error:%w", err) } @@ -1731,7 +1731,7 @@ func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string var matcher C.IpMatcher for _, ipcidr := range addresses { ipcidrLower := strings.ToLower(ipcidr) - if strings.Contains(ipcidrLower, "geoip:") { + if strings.HasPrefix(ipcidrLower, "geoip:") { subkeys := strings.Split(ipcidr, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1742,7 +1742,7 @@ func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string } matchers = append(matchers, matcher) } - } else if strings.Contains(ipcidrLower, "rule-set:") { + } else if strings.HasPrefix(ipcidrLower, "rule-set:") { subkeys := strings.Split(ipcidr, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1778,7 +1778,7 @@ func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapte var matcher C.DomainMatcher for _, domain := range domains { domainLower := strings.ToLower(domain) - if strings.Contains(domainLower, "geosite:") { + if strings.HasPrefix(domainLower, "geosite:") { subkeys := strings.Split(domain, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1789,7 +1789,7 @@ func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapte } matchers = append(matchers, matcher) } - } else if strings.Contains(domainLower, "rule-set:") { + } else if strings.HasPrefix(domainLower, "rule-set:") { subkeys := strings.Split(domain, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") diff --git a/clash-meta/hub/route/server.go b/clash-meta/hub/route/server.go index 6c47d67294..4e0d0c93f5 100644 --- a/clash-meta/hub/route/server.go +++ b/clash-meta/hub/route/server.go @@ -191,7 +191,7 @@ func startTLS(cfg *Config) { // handle tlsAddr if len(cfg.TLSAddr) > 0 { - cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(cfg.Certificate, cfg.PrivateKey) if err != nil { log.Errorln("External controller tls listen error: %s", err) return @@ -206,7 +206,9 @@ func startTLS(cfg *Config) { log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) tlsConfig := &tls.Config{Time: ntp.Now} tlsConfig.NextProtos = []string{"h2", "http/1.1"} - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(cfg.ClientAuthType) if len(cfg.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { @@ -214,7 +216,7 @@ func startTLS(cfg *Config) { } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(cfg.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(cfg.ClientAuthCert) if err != nil { log.Errorln("External controller tls listen error: %s", err) return @@ -223,7 +225,7 @@ func startTLS(cfg *Config) { } if cfg.EchKey != "" { - err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(cfg.EchKey, tlsConfig) if err != nil { log.Errorln("External controller tls serve error: %s", err) return diff --git a/clash-meta/listener/anytls/server.go b/clash-meta/listener/anytls/server.go index 731f13947e..0d35d4d6af 100644 --- a/clash-meta/listener/anytls/server.go +++ b/clash-meta/listener/anytls/server.go @@ -45,14 +45,16 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tls.Config{Time: ntp.Now} if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -65,7 +67,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } @@ -108,7 +110,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - if len(tlsConfig.Certificates) > 0 { + if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else { return nil, errors.New("disallow using AnyTLS without certificates config") diff --git a/clash-meta/listener/http/server.go b/clash-meta/listener/http/server.go index 2aba6fda4d..2c537dedd0 100644 --- a/clash-meta/listener/http/server.go +++ b/clash-meta/listener/http/server.go @@ -71,14 +71,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -91,14 +93,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -112,7 +114,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } diff --git a/clash-meta/listener/mixed/mixed.go b/clash-meta/listener/mixed/mixed.go index 995822b0cf..bc67a476e2 100644 --- a/clash-meta/listener/mixed/mixed.go +++ b/clash-meta/listener/mixed/mixed.go @@ -67,14 +67,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -87,14 +89,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -108,7 +110,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } diff --git a/clash-meta/listener/sing_hysteria2/server.go b/clash-meta/listener/sing_hysteria2/server.go index becb06b115..493607d525 100644 --- a/clash-meta/listener/sing_hysteria2/server.go +++ b/clash-meta/listener/sing_hysteria2/server.go @@ -56,15 +56,17 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi sl = &Listener{false, config, nil, nil} - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } tlsConfig := &tls.Config{ Time: ntp.Now, MinVersion: tls.VersionTLS13, } - tlsConfig.Certificates = []tls.Certificate{cert} + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) + if err != nil { + return nil, err + } + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { @@ -72,7 +74,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } @@ -80,7 +82,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } diff --git a/clash-meta/listener/sing_vless/server.go b/clash-meta/listener/sing_vless/server.go index 049f5eb1d9..83ba577924 100644 --- a/clash-meta/listener/sing_vless/server.go +++ b/clash-meta/listener/sing_vless/server.go @@ -81,14 +81,16 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -101,14 +103,14 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -153,7 +155,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if sl.decryption == nil { return nil, errors.New("disallow using Vless without any certificates/reality/decryption config") diff --git a/clash-meta/listener/sing_vmess/server.go b/clash-meta/listener/sing_vmess/server.go index 956aa70871..7dd0a163ba 100644 --- a/clash-meta/listener/sing_vmess/server.go +++ b/clash-meta/listener/sing_vmess/server.go @@ -81,14 +81,16 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -101,14 +103,14 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -153,7 +155,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } sl.listeners = append(sl.listeners, l) diff --git a/clash-meta/listener/socks/tcp.go b/clash-meta/listener/socks/tcp.go index 55e9e59439..45de213530 100644 --- a/clash-meta/listener/socks/tcp.go +++ b/clash-meta/listener/socks/tcp.go @@ -66,14 +66,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -86,14 +88,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -107,7 +109,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } diff --git a/clash-meta/listener/trojan/server.go b/clash-meta/listener/trojan/server.go index 6155d20927..e3c1002c9f 100644 --- a/clash-meta/listener/trojan/server.go +++ b/clash-meta/listener/trojan/server.go @@ -76,14 +76,16 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -96,14 +98,14 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -148,7 +150,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if !config.TrojanSSOption.Enabled { return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") diff --git a/clash-meta/listener/tuic/server.go b/clash-meta/listener/tuic/server.go index 30845515e7..7e659e596a 100644 --- a/clash-meta/listener/tuic/server.go +++ b/clash-meta/listener/tuic/server.go @@ -49,15 +49,17 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( return nil, err } - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } tlsConfig := &tls.Config{ Time: ntp.Now, MinVersion: tls.VersionTLS13, } - tlsConfig.Certificates = []tls.Certificate{cert} + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) + if err != nil { + return nil, err + } + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { @@ -65,7 +67,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } @@ -73,7 +75,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } diff --git a/clash-nyanpasu/.github/workflows/ci.yml b/clash-nyanpasu/.github/workflows/ci.yml index 8f644fac29..f88103e3e0 100644 --- a/clash-nyanpasu/.github/workflows/ci.yml +++ b/clash-nyanpasu/.github/workflows/ci.yml @@ -27,8 +27,8 @@ jobs: - uses: actions/checkout@v5 - name: Rust run: | - rustup toolchain install stable --profile minimal --no-self-update - rustup default stable + rustup toolchain install nightly --profile minimal --no-self-update + rustup default nightly rustup component add clippy rustfmt rustc --version cargo --version diff --git a/clash-nyanpasu/.github/workflows/deps-build-linux.yaml b/clash-nyanpasu/.github/workflows/deps-build-linux.yaml index 1eb8d6ba6e..27d467b1b1 100644 --- a/clash-nyanpasu/.github/workflows/deps-build-linux.yaml +++ b/clash-nyanpasu/.github/workflows/deps-build-linux.yaml @@ -49,10 +49,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - - name: Install Rust stable + - name: Install Rust nightly run: | - rustup install stable --profile minimal --no-self-update - rustup default stable + rustup install nightly --profile minimal --no-self-update + rustup default nightly - name: Setup Cargo binstall if: ${{ inputs.arch != 'x86_64' }} diff --git a/clash-nyanpasu/.github/workflows/deps-build-macos.yaml b/clash-nyanpasu/.github/workflows/deps-build-macos.yaml index 0be70ff28f..0a9ab05d74 100644 --- a/clash-nyanpasu/.github/workflows/deps-build-macos.yaml +++ b/clash-nyanpasu/.github/workflows/deps-build-macos.yaml @@ -51,10 +51,10 @@ jobs: with: xcode-version: 16 - - name: install Rust stable + - name: install Rust nightly run: | - rustup install stable --profile minimal --no-self-update - rustup default stable + rustup install nightly --profile minimal --no-self-update + rustup default nightly - name: Install Rust intel target if: ${{ inputs.aarch64 == false }} diff --git a/clash-nyanpasu/.github/workflows/deps-build-windows-nsis.yaml b/clash-nyanpasu/.github/workflows/deps-build-windows-nsis.yaml index e1ab01827f..3c45d78377 100644 --- a/clash-nyanpasu/.github/workflows/deps-build-windows-nsis.yaml +++ b/clash-nyanpasu/.github/workflows/deps-build-windows-nsis.yaml @@ -75,10 +75,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - - name: Install Rust stable + - name: Install Rust nightly run: | - rustup install stable --profile minimal --no-self-update - rustup default stable + rustup install nightly --profile minimal --no-self-update + rustup default nightly - name: Setup Rust target if: ${{ inputs.arch != 'x86_64' }} diff --git a/clash-nyanpasu/.github/workflows/macos-aarch64.yaml b/clash-nyanpasu/.github/workflows/macos-aarch64.yaml index 1b27b62e21..c23c06c1de 100644 --- a/clash-nyanpasu/.github/workflows/macos-aarch64.yaml +++ b/clash-nyanpasu/.github/workflows/macos-aarch64.yaml @@ -13,15 +13,15 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - - name: install Rust stable + - name: install Rust nightly run: | - rustup install stable --profile minimal --no-self-update - rustup default stable + rustup install nightly --profile minimal --no-self-update + rustup default nightly - uses: Swatinem/rust-cache@v2 with: workspaces: './backend/' - prefix-key: 'rust-stable' + prefix-key: 'rust-nightly' key: 'macos-13' shared-key: 'release' - uses: maxim-lobanov/setup-xcode@v1 diff --git a/clash-nyanpasu/.lintstagedrc.js b/clash-nyanpasu/.lintstagedrc.js index 330d341d70..278366d955 100644 --- a/clash-nyanpasu/.lintstagedrc.js +++ b/clash-nyanpasu/.lintstagedrc.js @@ -1,5 +1,19 @@ export default { - '*.{js,cjs,.mjs,jsx}': ['prettier --write', 'eslint --cache --fix'], + '*.{js,cjs,.mjs,jsx}': (filenames) => { + const configFiles = [ + 'eslint.config.js', + '.lintstagedrc.js', + 'commitlint.config.js', + ] + const filtered = filenames.filter( + (file) => !configFiles.some((config) => file.endsWith(config)), + ) + if (filtered.length === 0) return [] + return [ + `prettier --write ${filtered.join(' ')}`, + `eslint --cache --fix ${filtered.join(' ')}`, + ] + }, 'scripts/**/*.{ts,tsx}': [ 'prettier --write', 'eslint --cache --fix', diff --git a/clash-nyanpasu/.prettierignore b/clash-nyanpasu/.prettierignore index 8b9a24961f..c048b5bab3 100644 --- a/clash-nyanpasu/.prettierignore +++ b/clash-nyanpasu/.prettierignore @@ -8,4 +8,5 @@ pnpm-lock.yaml *.wxs frontend/nyanpasu/src/route-tree.gen.ts frontend/nyanpasu/auto-imports.d.ts +frontend/nyanpasu/src/paraglide/ backend/tauri/gen/schemas/ diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index 3030173d8e..f3ac23b084 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -4457,7 +4457,7 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "include-compress-bytes" version = "0.1.0" -source = "git+https://github.com/libnyanpasu/include-compress-bytes?rev=250a12c#250a12c13a8fed1cd2c0476e01c3dd89555840d2" +source = "git+https://github.com/libnyanpasu/include-compress-bytes?rev=4e4f25b#4e4f25b794ed0b9a323a893bcdaf22f5024ab58a" dependencies = [ "brotli", "cargo-emit 0.2.1", @@ -4470,7 +4470,7 @@ dependencies = [ [[package]] name = "include_url_macro" version = "0.1.0" -source = "git+https://github.com/libnyanpasu/include_url_macro?rev=b0b88e5#b0b88e5dd0685e607c6d965420fc2a035b4a0efe" +source = "git+https://github.com/libnyanpasu/include_url_macro?rev=fbe47bd#fbe47bd71047892a218e3166a2a1bac474488218" dependencies = [ "brotli", "bytes", diff --git a/clash-nyanpasu/backend/boa_utils/Cargo.toml b/clash-nyanpasu/backend/boa_utils/Cargo.toml index a705bac3dd..6c05a5fdd3 100644 --- a/clash-nyanpasu/backend/boa_utils/Cargo.toml +++ b/clash-nyanpasu/backend/boa_utils/Cargo.toml @@ -24,8 +24,8 @@ tracing = "0.1" url = "2" log = "0.4" anyhow = "1.0" -include_url_macro = { git = "https://github.com/libnyanpasu/include_url_macro", rev = "b0b88e5" } -include-compress-bytes = { git = "https://github.com/libnyanpasu/include-compress-bytes", rev = "250a12c" } +include_url_macro = { git = "https://github.com/libnyanpasu/include_url_macro", rev = "fbe47bd" } +include-compress-bytes = { git = "https://github.com/libnyanpasu/include-compress-bytes", rev = "4e4f25b" } phf = { version = "0.13.1", features = ["macros"] } # for cacheing diff --git a/clash-nyanpasu/eslint.config.js b/clash-nyanpasu/eslint.config.js index ceb584891e..e39731a8ff 100644 --- a/clash-nyanpasu/eslint.config.js +++ b/clash-nyanpasu/eslint.config.js @@ -31,6 +31,10 @@ const ignores = [ 'dist/', 'backend/', 'backend/**/target', + 'scripts/deno/**', + 'eslint.config.js', + '.lintstagedrc.js', + 'commitlint.config.js', ] export default tseslint.config( @@ -80,7 +84,12 @@ export default tseslint.config( { files: ['**/*.{ts,tsx,mtsx}'], extends: [...tseslint.configs.recommended], - ignores: [...ignores, '**/vite.config.ts', '**/tailwind.config.ts'], + ignores: [ + ...ignores, + 'frontend/nyanpasu/vite.config.ts', + 'frontend/nyanpasu/tailwind.config.ts', + 'frontend/ui/vite.config.ts', + ], rules: { '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-explicit-any': 'warn', @@ -93,7 +102,10 @@ export default tseslint.config( }, }, { - files: ['**/vite.config.ts', '**/tailwind.config.ts'], + files: [ + 'frontend/nyanpasu/vite.config.ts', + 'frontend/nyanpasu/tailwind.config.ts', + ], extends: [...tseslint.configs.recommended], rules: { '@typescript-eslint/no-unused-vars': 'warn', @@ -105,6 +117,19 @@ export default tseslint.config( }, }, }, + { + files: ['frontend/ui/vite.config.ts'], + extends: [...tseslint.configs.recommended], + rules: { + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + }, + languageOptions: { + parserOptions: { + project: './frontend/ui/tsconfig.json', + }, + }, + }, { files: ['**/*.{jsx,mjsx,tsx,mtsx}'], languageOptions: { diff --git a/clash-nyanpasu/frontend/nyanpasu/messages/en.json b/clash-nyanpasu/frontend/nyanpasu/messages/en.json index ec447d095a..d97c5ba8bd 100644 --- a/clash-nyanpasu/frontend/nyanpasu/messages/en.json +++ b/clash-nyanpasu/frontend/nyanpasu/messages/en.json @@ -17,5 +17,18 @@ "settings_system_proxy_proxy_bypass_label": "System Proxy Bypass", "settings_system_proxy_current_system_proxy_label": "Current System Proxy", "settings_user_interface_title": "User Interface", - "unit_seconds": "s" + "settings_user_interface_language_label": "Language", + "settings_user_interface_theme_mode_label": "Theme Mode", + "settings_user_interface_theme_mode_light": "Light", + "settings_user_interface_theme_mode_dark": "Dark", + "settings_user_interface_theme_mode_system": "System", + "settings_user_interface_theme_color_label": "Theme Color", + "settings_clash_settings_title": "Clash Settings", + "settings_clash_settings_allow_lan_label": "Allow LAN", + "settings_clash_settings_ipv6_label": "Enable IPv6", + "settings_clash_settings_tun_stack_label": "TUN Stack", + "settings_clash_settings_log_level_label": "Log Level", + "unit_seconds": "s", + "common_submit": "Submit", + "common_cancel": "Cancel" } diff --git a/clash-nyanpasu/frontend/nyanpasu/messages/ru.json b/clash-nyanpasu/frontend/nyanpasu/messages/ru.json index 353f3fc9d0..c756167bb6 100644 --- a/clash-nyanpasu/frontend/nyanpasu/messages/ru.json +++ b/clash-nyanpasu/frontend/nyanpasu/messages/ru.json @@ -17,5 +17,18 @@ "settings_system_proxy_proxy_bypass_label": "Обход прокси", "settings_system_proxy_current_system_proxy_label": "Текущий системный прокси", "settings_user_interface_title": "Интерфейс пользователя", - "unit_seconds": "секунды" + "settings_user_interface_language_label": "Язык", + "settings_user_interface_theme_mode_label": "Режим темы", + "settings_user_interface_theme_mode_light": "Светлый", + "settings_user_interface_theme_mode_dark": "Темный", + "settings_user_interface_theme_mode_system": "Системный", + "settings_user_interface_theme_color_label": "Цвет темы", + "settings_clash_settings_title": "Настройки Clash", + "settings_clash_settings_allow_lan_label": "Разрешить LAN", + "settings_clash_settings_ipv6_label": "Включить IPv6", + "settings_clash_settings_tun_stack_label": "TUN Stack", + "settings_clash_settings_log_level_label": "Уровень журнала", + "unit_seconds": "секунды", + "common_submit": "Отправить", + "common_cancel": "Отменить" } diff --git a/clash-nyanpasu/frontend/nyanpasu/messages/zh-cn.json b/clash-nyanpasu/frontend/nyanpasu/messages/zh-cn.json index d731200aab..b795099c0a 100644 --- a/clash-nyanpasu/frontend/nyanpasu/messages/zh-cn.json +++ b/clash-nyanpasu/frontend/nyanpasu/messages/zh-cn.json @@ -17,5 +17,18 @@ "settings_system_proxy_proxy_bypass_label": "系统代理绕过", "settings_system_proxy_current_system_proxy_label": "当前系统代理", "settings_user_interface_title": "用户界面", - "unit_seconds": "秒" + "settings_user_interface_language_label": "语言", + "settings_user_interface_theme_mode_label": "主题模式", + "settings_user_interface_theme_mode_light": "浅色", + "settings_user_interface_theme_mode_dark": "深色", + "settings_user_interface_theme_mode_system": "跟随系统", + "settings_user_interface_theme_color_label": "主题颜色", + "settings_clash_settings_title": "Clash 设置", + "settings_clash_settings_allow_lan_label": "允许局域网连接", + "settings_clash_settings_ipv6_label": "启用 IPv6", + "settings_clash_settings_tun_stack_label": "TUN 堆栈", + "settings_clash_settings_log_level_label": "日志级别", + "unit_seconds": "秒", + "common_submit": "提交", + "common_cancel": "取消" } diff --git a/clash-nyanpasu/frontend/nyanpasu/messages/zh-tw.json b/clash-nyanpasu/frontend/nyanpasu/messages/zh-tw.json index 1a3dbaf0f9..7e2b14594f 100644 --- a/clash-nyanpasu/frontend/nyanpasu/messages/zh-tw.json +++ b/clash-nyanpasu/frontend/nyanpasu/messages/zh-tw.json @@ -17,5 +17,18 @@ "settings_system_proxy_proxy_bypass_label": "系統代理繞過", "settings_system_proxy_current_system_proxy_label": "當前系統代理", "settings_user_interface_title": "使用者介面", - "unit_seconds": "秒" + "settings_user_interface_language_label": "語言", + "settings_user_interface_theme_mode_label": "主題模式", + "settings_user_interface_theme_mode_light": "淺色", + "settings_user_interface_theme_mode_dark": "深色", + "settings_user_interface_theme_mode_system": "跟隨系統", + "settings_user_interface_theme_color_label": "主題顏色", + "settings_clash_settings_title": "Clash 設置", + "settings_clash_settings_allow_lan_label": "允許區域網路連線", + "settings_clash_settings_ipv6_label": "啟用 IPv6", + "settings_clash_settings_tun_stack_label": "TUN 堆棧", + "settings_clash_settings_log_level_label": "日誌級別", + "unit_seconds": "秒", + "common_submit": "提交", + "common_cancel": "取消" } diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 99c06ffc43..1cc8f99ff6 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -23,14 +23,18 @@ "@mui/x-date-pickers": "8.17.0", "@nyanpasu/interface": "workspace:^", "@nyanpasu/ui": "workspace:^", + "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", "@radix-ui/react-slot": "1.2.4", - "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-use-controllable-state": "1.2.2", "@tailwindcss/postcss": "4.1.17", "@tanstack/router-zod-adapter": "1.81.5", "@tauri-apps/api": "2.8.0", "@types/json-schema": "7.0.15", "@uidotdev/usehooks": "2.4.1", + "@uiw/react-color": "2.9.2", "ahooks": "3.9.6", "allotment": "1.20.4", "class-variance-authority": "0.7.1", @@ -93,7 +97,7 @@ "shiki": "2.5.0", "unplugin-auto-import": "20.3.0", "unplugin-icons": "22.5.0", - "validator": "13.15.23", + "validator": "13.15.26", "vite": "7.2.4", "vite-plugin-html": "3.2.2", "vite-plugin-sass-dts": "1.3.34", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/providers/language-provider.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/providers/language-provider.tsx new file mode 100644 index 0000000000..21233dc992 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/providers/language-provider.tsx @@ -0,0 +1,44 @@ +import { createContext, PropsWithChildren, useContext, useEffect } from 'react' +import { useLockFn } from '@/hooks/use-lock-fn' +import { getLocale, Locale, setLocale } from '@/paraglide/runtime' +import { useSetting } from '@nyanpasu/interface' + +const LanguageContext = createContext<{ + language?: Locale + setLanguage: (value: Locale) => Promise +} | null>(null) + +export const useLanguage = () => { + const context = useContext(LanguageContext) + + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider') + } + + return context +} + +export const LanguageProvider = ({ children }: PropsWithChildren) => { + const language = useSetting('language') + + const setLanguage = useLockFn(async (value: Locale) => { + await language.upsert(value) + }) + + useEffect(() => { + if (language.value && language.value !== getLocale()) { + setLocale(language.value as Locale) + } + }, [language.value]) + + return ( + + {children} + + ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/providers/theme-provider.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/providers/theme-provider.tsx index aa08f80436..77c59f2e8f 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/providers/theme-provider.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/providers/theme-provider.tsx @@ -14,10 +14,19 @@ import { themeFromSourceColor, } from '@material/material-color-utilities' import { useSetting } from '@nyanpasu/interface' +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' import { useLocalStorage } from '@uidotdev/usehooks' +const appWindow = getCurrentWebviewWindow() + export const DEFAULT_COLOR = '#1867C0' +export enum ThemeMode { + LIGHT = 'light', + DARK = 'dark', + SYSTEM = 'system', +} + const CUSTOM_THEME_KEY = 'custom-theme' as const const THEME_PALETTE_KEY = 'theme-palette-v1' as const @@ -52,11 +61,29 @@ const generateThemeCssVars = ({ schemes }: Theme) => { return lightCssVars + darkCssVars } +const changeHtmlThemeMode = (mode: Omit) => { + const root = document.documentElement + + if (mode === ThemeMode.DARK) { + root.classList.add(ThemeMode.DARK) + } else { + root.classList.remove(ThemeMode.DARK) + } + + if (mode === ThemeMode.LIGHT) { + root.classList.add(ThemeMode.LIGHT) + } else { + root.classList.remove(ThemeMode.LIGHT) + } +} + const ThemeContext = createContext<{ themePalette: Theme themeCssVars: string themeColor: string - setTheme: (color: string) => void + setThemeColor: (color: string) => Promise + themeMode: ThemeMode + setThemeMode: (mode: ThemeMode) => Promise } | null>(null) export function useExperimentalThemeContext() { @@ -72,13 +99,15 @@ export function useExperimentalThemeContext() { } export function ExperimentalThemeProvider({ children }: PropsWithChildren) { - const { value: themeColor } = useSetting('theme_color') + const themeMode = useSetting('theme_mode') + + const themeColor = useSetting('theme_color') const [cachedThemePalette, setCachedThemePalette] = useLocalStorage( THEME_PALETTE_KEY, themeFromSourceColor( // use default color if theme color is not set - argbFromHex(themeColor || DEFAULT_COLOR), + argbFromHex(themeColor.value || DEFAULT_COLOR), ), ) @@ -93,10 +122,12 @@ export function ExperimentalThemeProvider({ children }: PropsWithChildren) { insertStyle(CUSTOM_THEME_KEY, cachedThemeCssVars) }, [cachedThemeCssVars]) - const setTheme = useCallback( - (color: string) => { - if (color === themeColor) { + const setThemeColor = useCallback( + async (color: string) => { + if (color === themeColor.value) { return + } else { + await themeColor.upsert(color) } const materialColor = themeFromSourceColor( @@ -121,13 +152,42 @@ export function ExperimentalThemeProvider({ children }: PropsWithChildren) { ], ) + // listen to theme changed event and change html theme mode + useEffect(() => { + const unlisten = appWindow.onThemeChanged((e) => { + if (themeMode.value === ThemeMode.SYSTEM) { + changeHtmlThemeMode(e.payload) + } + }) + + return () => { + unlisten.then((fn) => fn()) + } + }, [themeMode.value]) + + const setThemeMode = useCallback( + async (mode: ThemeMode) => { + // if theme mode is not system, change html theme mode + if (mode !== ThemeMode.SYSTEM) { + changeHtmlThemeMode(mode) + } + + if (mode !== themeMode.value) { + await themeMode.upsert(mode) + } + }, + [themeMode], + ) + return ( {children} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx index 7ddbb04c2e..2533df5bf8 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx @@ -4,11 +4,13 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { isHexColor } from 'validator' import { atomIsDrawerOnlyIcon } from '@/store' +import { setEnabledExperimentalRouter } from '@/utils/experimental' import { languageOptions } from '@/utils/language' import Done from '@mui/icons-material/Done' import { Button, List, ListItem, ListItemText } from '@mui/material' import { useSetting } from '@nyanpasu/interface' import { BaseCard, Expand, MenuItem, SwitchItem } from '@nyanpasu/ui' +import { useNavigate } from '@tanstack/react-router' import { DEFAULT_COLOR } from '../layout/use-custom-theme' const commonSx = { @@ -106,6 +108,25 @@ const ThemeColor = () => { ) } +const ExperimentalSwitch = () => { + const navigate = useNavigate() + + const handleClick = () => { + setEnabledExperimentalRouter(true) + navigate({ to: '/experimental/dashboard' }) + } + + return ( + + + + + + ) +} + export const SettingNyanpasuUI = () => { const { t } = useTranslation() @@ -125,6 +146,8 @@ export const SettingNyanpasuUI = () => { checked={onlyIcon} onChange={() => setOnlyIcon(!onlyIcon)} /> + + ) diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/settings/system-proxy.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/settings/system-proxy.tsx index b49ff06dd5..60fbe32037 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/settings/system-proxy.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/settings/system-proxy.tsx @@ -21,7 +21,7 @@ const ProxyButton = ({ className={cn( 'group h-16 rounded-3xl font-bold', 'flex items-center justify-between gap-2', - 'data-[active=false]:bg-black', + 'data-[active=false]:bg-white dark:data-[active=false]:bg-black', className, )} data-active={String(Boolean(isActive))} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000000..bb3595012e --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,259 @@ +import Check from '~icons/material-symbols/check-rounded' +import RadioChecked from '~icons/material-symbols/radio-button-checked' +import Radio from '~icons/material-symbols/radio-button-unchecked' +import { AnimatePresence, motion } from 'framer-motion' +import { ComponentProps, createContext, useContext } from 'react' +import { cn } from '@nyanpasu/ui' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { useControllableState } from '@radix-ui/react-use-controllable-state' + +const DropdownMenuContext = createContext<{ + open: boolean +} | null>(null) + +const useDropdownMenuContext = () => { + const context = useContext(DropdownMenuContext) + + if (context === null) { + throw new Error( + 'DropdownMenu compound components cannot be rendered outside the DropdownMenu component', + ) + } + + return context +} + +export const DropdownMenu = ({ + open: inputOpen, + defaultOpen, + onOpenChange, + ...props +}: ComponentProps) => { + const [open, setOpen] = useControllableState({ + prop: inputOpen, + defaultProp: defaultOpen ?? false, + onChange: onOpenChange, + }) + + return ( + + + + ) +} + +export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +export const DropdownMenuGroup = DropdownMenuPrimitive.Group + +export const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +export const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroupContext = createContext<{ + value: string | null +}>({ value: null }) + +const useDropdownMenuRadioGroupContext = () => { + const context = useContext(DropdownMenuRadioGroupContext) + + if (context === undefined) { + throw new Error( + 'DropdownMenuRadioGroup compound components cannot be rendered outside the DropdownMenuRadioGroup component', + ) + } + + return context +} + +export const DropdownMenuRadioGroup = ({ + value: inputValue, + defaultValue, + onValueChange, + ...props +}: ComponentProps) => { + const [value, setValue] = useControllableState({ + prop: inputValue, + defaultProp: String(defaultValue), + onChange: onValueChange, + }) + + return ( + + + + ) +} + +export const DropdownMenuSubTrigger = DropdownMenuPrimitive.SubTrigger + +export const DropdownMenuSubContent = DropdownMenuPrimitive.SubContent + +export const DropdownMenuContent = ({ + children, + className, + ...props +}: ComponentProps) => { + const { open } = useDropdownMenuContext() + + return ( + + {open && ( + + + + {children} + + + + )} + + ) +} + +export const DropdownMenuItem = ({ + className, + ...props +}: ComponentProps) => { + return ( + + ) +} + +export const DropdownMenuCheckboxItem = ({ + children, + className, + ...props +}: ComponentProps) => { + return ( + + {children} + + + + + + ) +} + +export const DropdownMenuRadioItem = ({ + value, + children, + className, + ...props +}: ComponentProps) => { + const context = useDropdownMenuRadioGroupContext() + + const selected = context.value === value + + return ( + + + + + + {!selected && ( + + + + )} + +
{children}
+
+ ) +} + +export const DropdownMenuLabel = ({ + className, + ...props +}: ComponentProps) => { + return ( + + ) +} + +export const DropdownMenuSeparator = ({ + className, + ...props +}: ComponentProps) => { + return ( + + ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/select.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/select.tsx new file mode 100644 index 0000000000..f74277ddc2 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/select.tsx @@ -0,0 +1,505 @@ +import ArrowDropDown from '~icons/material-symbols/arrow-drop-down-rounded' +import Check from '~icons/material-symbols/check-rounded' +import { cva, type VariantProps } from 'class-variance-authority' +import { AnimatePresence, motion } from 'framer-motion' +import { + ComponentProps, + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react' +import { chains } from '@/utils/chain' +import { cn } from '@nyanpasu/ui' +import * as SelectPrimitive from '@radix-ui/react-select' +import { useControllableState } from '@radix-ui/react-use-controllable-state' + +export const selectTriggerVariants = cva( + [ + 'group relative box-border inline-flex w-full flex-auto items-baseline', + 'cursor-pointer', + 'px-4 py-4 outline-hidden', + // TODO: size variants, fix this + 'flex items-center justify-between h-14', + 'dark:text-on-surface', + ], + { + variants: { + variant: { + filled: 'rounded-t bg-surface-variant dark:bg-on-surface-variant', + // outlined use selectValuePlaceholderFieldsetVariants + outlined: '', + }, + }, + defaultVariants: { + variant: 'filled', + }, + }, +) + +export type SelectTriggerVariants = VariantProps + +export const selectLineVariants = cva('', { + variants: { + variant: { + filled: [ + 'absolute inset-x-0 bottom-0 w-full border-b border-on-primary-container', + 'transition-all duration-200', + + // pseudo elements be overlay parent element, will not affect the box size + 'after:absolute after:inset-x-0 after:bottom-0 after:z-10', + "after:scale-x-0 after:border-b-2 after:opacity-0 after:content-['']", + 'after:transition-all after:duration-200', + 'after:border-primary dark:after:border-on-primary-container', + + // sync parent group state, state from radix-ui + 'group-data-[state=open]:border-b-0', + 'group-data-[state=open]:after:scale-x-100', + 'group-data-[state=open]:after:opacity-100', + 'peer-focus:border-b-0', + 'peer-focus:after:scale-x-100', + 'peer-focus:after:opacity-100', + ], + // hidden line for outlined variant + outlined: 'hidden', + }, + }, + defaultVariants: { + variant: 'filled', + }, +}) + +export type SelectLineVariants = VariantProps + +export const selectValueVariants = cva('pointer-events-none', { + variants: { + variant: { + filled: '', + outlined: '', + }, + haveValue: { + true: '', + false: '', + }, + }, + compoundVariants: [ + { + variant: 'filled', + haveValue: true, + className: 'mt-3', + }, + ], + defaultVariants: { + variant: 'filled', + haveValue: false, + }, +}) + +export type SelectValueVariants = VariantProps + +export const selectValuePlaceholderVariants = cva( + [ + 'absolute', + 'left-4 top-4', + 'pointer-events-none', + 'text-base select-none', + // TODO: only transition position, not text color + 'transition-all duration-200', + ], + { + variants: { + variant: { + filled: [ + 'group-data-[state=open]:top-2 group-data-[state=open]:dark:text-surface', + 'group-data-[state=open]:text-xs group-data-[state=open]:text-primary', + ], + outlined: [ + 'group-data-[state=open]:-top-2', + 'group-data-[state=open]:text-sm', + 'group-data-[state=open]:text-primary', + + 'dark:group-data-[state=open]:text-inverse-primary', + 'dark:group-data-[state=closed]:text-on-primary-container', + ], + }, + focus: { + true: '', + false: '', + }, + }, + compoundVariants: [ + { + variant: 'filled', + focus: true, + className: 'top-2 text-xs', + }, + { + variant: 'outlined', + focus: true, + className: '-top-2 text-sm', + }, + ], + defaultVariants: { + variant: 'filled', + focus: false, + }, + }, +) + +export type SelectValuePlaceholderVariants = VariantProps< + typeof selectValuePlaceholderVariants +> + +export const selectValuePlaceholderFieldsetVariants = cva( + 'pointer-events-none', + { + variants: { + variant: { + // only for outlined variant + filled: 'hidden', + outlined: [ + 'absolute inset-0 text-left', + 'rounded transition-all duration-200', + // may open border width will be 1.5, idk + 'group-data-[state=closed]:border', + 'group-data-[state=open]:border-2', + 'peer-not-focus:border', + 'peer-focus:border-2', + // different material web border color, i think this looks better + 'group-data-[state=closed]:border-outline-variant', + 'group-data-[state=open]:border-primary', + 'peer-not-focus:border-primary-container', + 'peer-focus:border-primary', + // dark must be prefixed + 'dark:group-data-[state=closed]:border-outline-variant', + 'dark:group-data-[state=open]:border-primary-container', + 'dark:peer-not-focus:border-outline-variant', + 'dark:peer-focus:border-primary-container', + ], + }, + }, + defaultVariants: { + variant: 'filled', + }, + }, +) + +export type SelectValuePlaceholderFieldsetVariants = VariantProps< + typeof selectValuePlaceholderFieldsetVariants +> + +export const selectValuePlaceholderLegendVariants = cva('', { + variants: { + variant: { + // only for outlined variant + filled: 'hidden', + outlined: 'invisible ml-2 px-2 text-sm h-0', + }, + haveValue: { + true: '', + false: '', + }, + }, + compoundVariants: [ + { + variant: 'outlined', + haveValue: false, + className: 'group-data-[state=closed]:hidden group-not-focus:hidden', + }, + ], + defaultVariants: { + variant: 'filled', + haveValue: false, + }, +}) + +export type SelectValuePlaceholderLegendVariants = VariantProps< + typeof selectValuePlaceholderLegendVariants +> + +export const selectContentVariants = cva( + [ + 'relative w-full overflow-auto rounded shadow-container z-50', + 'bg-inverse-on-surface dark:bg-surface', + 'dark:text-on-surface', + ], + { + variants: { + variant: { + filled: 'rounded-t-none', + outlined: '', + }, + }, + defaultVariants: { + variant: 'filled', + }, + }, +) + +export type SelectContentVariants = VariantProps + +type SelectContextType = { + haveValue?: boolean + open?: boolean +} & SelectTriggerVariants + +const SelectContext = createContext(null) + +const useSelectContext = () => { + const context = useContext(SelectContext) + + if (!context) { + throw new Error('useSelectContext must be used within a SelectProvider') + } + + return context +} + +export const SelectLine = ({ className, ...props }: ComponentProps<'div'>) => { + const { variant } = useSelectContext() + + return ( +
+ ) +} + +export const Select = ({ + onValueChange, + variant, + open: inputOpen, + defaultOpen, + onOpenChange, + ...props +}: React.ComponentProps & + SelectTriggerVariants) => { + const [open, setOpen] = useControllableState({ + prop: inputOpen, + defaultProp: defaultOpen ?? false, + onChange: onOpenChange, + }) + + const [haveValue, setHaveValue] = useState( + Boolean(props.value || props.defaultValue), + ) + + const handleOnChange = useCallback((value?: string) => { + setHaveValue(Boolean(value)) + }, []) + + useEffect(() => { + setHaveValue(Boolean(props.value || props.defaultValue)) + }, [props.value, props.defaultValue]) + + return ( + + + + ) +} + +export type SelectProps = ComponentProps + +export const SelectValue = ({ + className, + placeholder, + ...props +}: ComponentProps) => { + const { haveValue, open, variant } = useSelectContext() + + return ( + <> +
+ +
+ +
+ + {placeholder} + +
+ +
+ {placeholder} +
+ + ) +} + +export const SelectGroup = ( + props: ComponentProps, +) => { + return +} + +export const SelectLabel = ({ + className, + ...props +}: ComponentProps) => { + return ( + + ) +} + +export const SelectTrigger = ({ + className, + children, + ...props +}: ComponentProps) => { + const { variant } = useSelectContext() + + return ( + + {children} + + + + + + ) +} + +export const SelectIcon = ({ + asChild, + children, + className, + ...props +}: ComponentProps) => { + return ( + + {asChild ? children : } + + ) +} + +export const SelectContent = ({ + className, + children, + ...props +}: ComponentProps) => { + const { open, variant } = useSelectContext() + + return ( + + {open && ( + + + + {children} + + + + )} + + ) +} + +export const SelectItem = ({ + className, + children, + ...props +}: ComponentProps) => { + return ( + + {children} + + + + + + ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/allow-lan-switch.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/allow-lan-switch.tsx new file mode 100644 index 0000000000..fbaff318b3 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/allow-lan-switch.tsx @@ -0,0 +1,43 @@ +import { useMemo } from 'react' +import { Switch } from '@/components/ui/switch' +import { useLockFn } from '@/hooks/use-lock-fn' +import { m } from '@/paraglide/messages' +import { message } from '@/utils/notification' +import { useClashConfig } from '@nyanpasu/interface' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function AllowLanSwitch() { + const { query, upsert } = useClashConfig() + + const value = useMemo(() => query.data?.['allow-lan'], [query.data]) + + const handleAllowLan = useLockFn(async (input: boolean) => { + try { + await upsert.mutateAsync({ + 'allow-lan': input, + }) + } catch (error) { + message(`Activation Allow LAN failed!`, { + title: 'Error', + kind: 'error', + }) + } + }) + + return ( + + +
{m.settings_clash_settings_allow_lan_label()}
+ + +
+
+ ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/ipv6-switch.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/ipv6-switch.tsx new file mode 100644 index 0000000000..40bae59583 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/ipv6-switch.tsx @@ -0,0 +1,43 @@ +import { useMemo } from 'react' +import { Switch } from '@/components/ui/switch' +import { useLockFn } from '@/hooks/use-lock-fn' +import { m } from '@/paraglide/messages' +import { message } from '@/utils/notification' +import { useClashConfig } from '@nyanpasu/interface' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function IPv6Switch() { + const { query, upsert } = useClashConfig() + + const value = useMemo(() => query.data?.['ipv6'], [query.data]) + + const handleIPv6 = useLockFn(async (input: boolean) => { + try { + await upsert.mutateAsync({ + ipv6: input, + }) + } catch (error) { + message(`Activation IPv6 failed!`, { + title: 'Error', + kind: 'error', + }) + } + }) + + return ( + + +
{m.settings_clash_settings_ipv6_label()}
+ + +
+
+ ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/log-level-selector.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/log-level-selector.tsx new file mode 100644 index 0000000000..add776ca33 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/log-level-selector.tsx @@ -0,0 +1,68 @@ +import { useCallback, useMemo } from 'react' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { m } from '@/paraglide/messages' +import { useClashConfig } from '@nyanpasu/interface' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +const LOG_LEVEL_OPTIONS = { + debug: 'Debug', + info: 'Info', + warning: 'Warn', + error: 'Error', + silent: 'Silent', +} as const + +export default function LogLevelSelector() { + const { query, upsert } = useClashConfig() + + const value = useMemo( + () => query.data?.['log-level'] as keyof typeof LOG_LEVEL_OPTIONS, + [query.data], + ) + + const handleLogLevelChange = useCallback( + async (value: string) => { + await upsert.mutateAsync({ + 'log-level': value as string, + }) + }, + [upsert], + ) + + return ( + + + + + + ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/tun-stack-selector.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/tun-stack-selector.tsx new file mode 100644 index 0000000000..2cc221a1ce --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/_modules/tun-stack-selector.tsx @@ -0,0 +1,101 @@ +import { useCallback, useMemo } from 'react' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { useCoreType } from '@/hooks/use-store' +import { m } from '@/paraglide/messages' +import { formatError } from '@/utils' +import { message } from '@/utils/notification' +import { TunStack, useRuntimeProfile, useSetting } from '@nyanpasu/interface' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function TunStackSelector() { + const [coreType] = useCoreType() + + const tunStack = useSetting('tun_stack') + + const enableTunMode = useSetting('enable_tun_mode') + + const runtimeProfile = useRuntimeProfile() + + const tunStackOptions = useMemo(() => { + const options: { + [key: string]: string + } = { + system: 'System', + gvisor: 'gVisor', + mixed: 'Mixed', + } + + // clash not support mixed + if (coreType === 'clash') { + delete options.mixed + } + return options + }, [coreType]) + + const currentTunStack = useMemo(() => { + const stack = tunStack.value || 'gvisor' + return stack in tunStackOptions ? stack : 'gvisor' + }, [tunStackOptions, tunStack.value]) + + const handleTunStackChange = useCallback( + async (value: string) => { + try { + await tunStack.upsert(value as TunStack) + + if (enableTunMode.value) { + // just to reload clash config + await enableTunMode.upsert(true) + } + + // need manual mutate to refetch runtime profile + await runtimeProfile.refetch() + } catch (error) { + message(`Change Tun Stack failed ! \n Error: ${formatError(error)}`, { + title: 'Error', + kind: 'error', + }) + } + }, + [tunStack, enableTunMode, runtimeProfile], + ) + + return ( + + + + + + ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/route.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/route.tsx index 232ec76daa..6a9789595f 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/route.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/clash-settings/route.tsx @@ -1,4 +1,13 @@ +import { m } from '@/paraglide/messages' import { createFileRoute } from '@tanstack/react-router' +import { + SettingsTitle, + SettingsTitlePlaceholder, +} from '../_modules/settings-title' +import AllowLanSwitch from './_modules/allow-lan-switch' +import IPv6Switch from './_modules/ipv6-switch' +import LogLevelSelector from './_modules/log-level-selector' +import TunStackSelector from './_modules/tun-stack-selector' export const Route = createFileRoute( '/(experimental)/experimental/settings/clash-settings', @@ -8,6 +17,17 @@ export const Route = createFileRoute( function RouteComponent() { return ( -
Hello "/(experimental)/experimental/settings/clash-settings"!
+ <> + + {m.settings_system_proxy_title()} + + + + + + + + + ) } diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/experimental-switch.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/experimental-switch.tsx new file mode 100644 index 0000000000..792d40103d --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/experimental-switch.tsx @@ -0,0 +1,27 @@ +import { Button } from '@/components/ui/button' +import { setEnabledExperimentalRouter } from '@/utils/experimental' +import { useNavigate } from '@tanstack/react-router' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function ExperimentalSwitch() { + const navigate = useNavigate() + const handleClick = () => { + setEnabledExperimentalRouter(false) + navigate({ to: '/' }) + } + + return ( + + +
Switch to Legacy UI
+ + +
+
+ ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/language-selector.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/language-selector.tsx new file mode 100644 index 0000000000..85b5d1b89e --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/language-selector.tsx @@ -0,0 +1,50 @@ +import { useLanguage } from '@/components/providers/language-provider' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { m } from '@/paraglide/messages' +import { Locale, locales } from '@/paraglide/runtime' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function LanguageSelector() { + const { language, setLanguage } = useLanguage() + + const handleLanguageChange = (value: string) => { + setLanguage(value as Locale) + } + + return ( + + +
{m.settings_user_interface_language_label()}
+ + + + + + + + + {locales.map((value) => ( + + {m.language(value, { locale: value })} + + ))} + + + +
+
+ ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/theme-color-config.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/theme-color-config.tsx new file mode 100644 index 0000000000..552b1eb47a --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/theme-color-config.tsx @@ -0,0 +1,68 @@ +import Check from '~icons/material-symbols/check-rounded' +import { useCallback, useState } from 'react' +import { useExperimentalThemeContext } from '@/components/providers/theme-provider' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { m } from '@/paraglide/messages' +import { Wheel } from '@uiw/react-color' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function ThemeColorConfig() { + const { themeColor, setThemeColor } = useExperimentalThemeContext() + + const [open, setOpen] = useState(false) + + const [cachedThemeColor, setCachedThemeColor] = useState(themeColor) + + const handleSubmit = useCallback(async () => { + setOpen(false) + await setThemeColor(cachedThemeColor) + }, [cachedThemeColor, setThemeColor]) + + return ( + + +
{m.settings_user_interface_theme_color_label()}
+ + + + + + + + { + setCachedThemeColor(color.hex) + }} + /> + + + + +
+
+ ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/theme-mode-selector.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/theme-mode-selector.tsx new file mode 100644 index 0000000000..60df50da67 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/_modules/theme-mode-selector.tsx @@ -0,0 +1,58 @@ +import { + ThemeMode, + useExperimentalThemeContext, +} from '@/components/providers/theme-provider' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { m } from '@/paraglide/messages' +import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card' + +export default function ThemeModeSelector() { + const { themeMode, setThemeMode } = useExperimentalThemeContext() + + const handleThemeModeChange = (value: string) => { + setThemeMode(value as ThemeMode) + } + + const messages = { + [ThemeMode.LIGHT]: m.settings_user_interface_theme_mode_light(), + [ThemeMode.DARK]: m.settings_user_interface_theme_mode_dark(), + [ThemeMode.SYSTEM]: m.settings_user_interface_theme_mode_system(), + } satisfies Record + + return ( + + +
{m.settings_user_interface_theme_mode_label()}
+ + + + + + + + + {Object.values(ThemeMode).map((value) => ( + + {value} + + ))} + + + +
+
+ ) +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/route.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/route.tsx index 989b75c2b2..685e07198d 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/route.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(experimental)/experimental/settings/user-interface/route.tsx @@ -4,6 +4,10 @@ import { SettingsTitle, SettingsTitlePlaceholder, } from '../_modules/settings-title' +import ExperimentalSwitch from './_modules/experimental-switch' +import LanguageSelector from './_modules/language-selector' +import ThemeColorConfig from './_modules/theme-color-config' +import ThemeModeSelector from './_modules/theme-mode-selector' export const Route = createFileRoute( '/(experimental)/experimental/settings/user-interface', @@ -23,6 +27,14 @@ function RouteComponent() { <> {m.settings_user_interface_title()} + + + + + + + + ) } diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/__root.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/__root.tsx index 60e5a4b17f..8784a2512d 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/__root.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/__root.tsx @@ -19,6 +19,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat' import relativeTime from 'dayjs/plugin/relativeTime' import { lazy } from 'react' import { BlockTaskProvider } from '@/components/providers/block-task-provider' +import { LanguageProvider } from '@/components/providers/language-provider' import { ExperimentalThemeProvider } from '@/components/providers/theme-provider' import { NyanpasuProvider } from '@nyanpasu/interface' import styles from './-__root.module.scss' @@ -74,17 +75,19 @@ export default function App() { return ( - - - - + + + + + - - - - + + + + - + + ) diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index 19a40a9d45..d34d1057a9 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.17", - "mihomo_alpha": "alpha-bc8f0dc", + "mihomo_alpha": "alpha-17966b5", "clash_rs": "v0.9.3", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.3-alpha+sha.a6538ac" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2025-12-17T22:21:31.955Z" + "updated_at": "2025-12-18T22:21:30.469Z" } diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 260cfc6ee1..125c058a2d 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -35,6 +35,7 @@ "fmt": "run-p fmt:*", "fmt:backend": "cargo fmt --manifest-path ./backend/Cargo.toml --all", "fmt:prettier": "prettier --write .", + "fmt:eslint": "eslint --cache --fix . eslint.config.js", "updater": "tsx scripts/updater.ts", "updater:nightly": "tsx scripts/updater-nightly.ts", "send-notify": "tsx scripts/telegram-notify.ts", @@ -71,7 +72,7 @@ "autoprefixer": "10.4.23", "conventional-changelog-conventionalcommits": "9.1.0", "cross-env": "10.1.0", - "dedent": "1.7.0", + "dedent": "1.7.1", "eslint": "9.39.2", "eslint-config-prettier": "10.1.8", "eslint-import-resolver-alias": "1.1.2", @@ -108,7 +109,7 @@ "typescript": "5.9.3", "typescript-eslint": "8.50.0" }, - "packageManager": "pnpm@10.20.0", + "packageManager": "pnpm@10.26.1", "engines": { "node": "22.21.1" }, diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 781b65607e..eebb4d699d 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -65,8 +65,8 @@ importers: specifier: 10.1.0 version: 10.1.0 dedent: - specifier: 1.7.0 - version: 1.7.0(babel-plugin-macros@3.1.0) + specifier: 1.7.1 + version: 1.7.1(babel-plugin-macros@3.1.0) eslint: specifier: 9.39.2 version: 9.39.2(jiti@2.6.1) @@ -248,15 +248,24 @@ importers: '@nyanpasu/ui': specifier: workspace:^ version: link:../ui + '@radix-ui/react-dropdown-menu': + specifier: 2.1.16 + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-scroll-area': specifier: 1.2.10 version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-select': + specifier: 2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-slot': specifier: 1.2.4 version: 1.2.4(@types/react@19.2.7)(react@19.2.0) '@radix-ui/react-switch': - specifier: ^1.2.6 + specifier: 1.2.6 version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': + specifier: 1.2.2 + version: 1.2.2(@types/react@19.2.7)(react@19.2.0) '@tailwindcss/postcss': specifier: 4.1.17 version: 4.1.17 @@ -272,6 +281,9 @@ importers: '@uidotdev/usehooks': specifier: 2.4.1 version: 2.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color': + specifier: 2.9.2 + version: 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ahooks: specifier: 3.9.6 version: 3.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -454,8 +466,8 @@ importers: specifier: 22.5.0 version: 22.5.0(@svgr/core@8.1.0(typescript@5.9.3)) validator: - specifier: 13.15.23 - version: 13.15.23 + specifier: 13.15.26 + version: 13.15.26 vite: specifier: 7.2.4 version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) @@ -612,8 +624,8 @@ importers: specifier: 3.4.2 version: 3.4.2 fs-extra: - specifier: 11.3.2 - version: 11.3.2 + specifier: 11.3.3 + version: 11.3.3 octokit: specifier: 5.0.5 version: 5.0.5 @@ -1806,6 +1818,21 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@greenhat616/material-react-table@4.0.0': resolution: {integrity: sha512-CGjE4MaM2Pamdgli7gJCx0PCimq1qlnKl3/AlFLsdgGMzVyJHmghQ0rL11TS95i3UEPNsPCFTmtUHU4sCJhDHA==} engines: {node: '>=16'} @@ -2653,6 +2680,32 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -2680,6 +2733,89 @@ packages: '@types/react': optional: true + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.10': resolution: {integrity: sha512-4kY9IVa6+9nJPsYmngK5Uk2kUmZnv7ChhHAFeQ5oaj8jrR1bIi3xww8nH71pz1/Ve4d/cXO3YxT8eikt1B0a8w==} peerDependencies: @@ -2693,6 +2829,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.5': resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} peerDependencies: @@ -2732,6 +2881,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-scroll-area@1.2.10': resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} peerDependencies: @@ -2745,6 +2907,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -2803,6 +2978,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -2821,6 +3005,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-size@1.1.1': resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} peerDependencies: @@ -2830,6 +3023,22 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} @@ -3811,6 +4020,156 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' + '@uiw/color-convert@2.9.2': + resolution: {integrity: sha512-ibw9OS29S7GlL+vDwU3p5XG3vhR7XdzUecydpZbakUeg2Td6nfsnrCAX9sbLwQ73p0abO42v+V4qRaWq+7/BjQ==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + + '@uiw/react-color-alpha@2.9.2': + resolution: {integrity: sha512-a2ACkE2vZIS4xnN9DaRfkQtAX/t8oK5NRSbX2QeOL23WIMHP1VNs7Yq5gXB68RHYenFgvs2JHuMOxZ2mK1W5Mw==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-block@2.9.2': + resolution: {integrity: sha512-0EIZTELA5pnxyMlBOFo3hrpy73db+Qeq6E+QptNfD/8izor8OvY1Uquj2VqD6gDz+iVHMELIoKxpaQ8sZR7NOg==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-chrome@2.9.2': + resolution: {integrity: sha512-p7OZB7VWrkVbHxcTHsAq5U2vt3hAP3VvKEiDi592LKxS11IMnSd15ta8ngbJaXZWatqEpJSNgj12581yHtx+Bg==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-circle@2.9.2': + resolution: {integrity: sha512-7XaeX3LfCRkZKffHL/KtYps7I9hNpmx9sJOuwi0ML+3urToFD8s7Iuq3upYZt8REYt1Y84SBjuUqx2YYmjUEjA==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-colorful@2.9.2': + resolution: {integrity: sha512-tz/xeHayna2wpLipkZmcMgL1rmLMxfAmlOyBhUeWrpvqb9Fx59C/wL+5IYJA4rdsQvr9WyWjWmU/GhVKsEkW9w==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-compact@2.9.2': + resolution: {integrity: sha512-fSSgRRBjkYuGRebZ7XM5XGVFvFwEyEGaV6mOhUpr3JFKhIES0/9oPbd80GDbRdj57Zxxrj76MCtd/aCFfwQSWA==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-editable-input-hsla@2.9.2': + resolution: {integrity: sha512-kZfj3W20msLeP8/HY498rG30eBHRPAyxduhu94HLa9XggT/0ogwA9xZJZgWd6B7FYPeRlhRDY7dnF7caND63GQ==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-editable-input-rgba@2.9.2': + resolution: {integrity: sha512-sQW3tSao754954aQuVK4qvn1i+KC2piE4UftaBubD3QxC02gg5VfgZRoI6AV+nLr73Ifv3mCXewjN1BcP/+x4A==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-editable-input@2.9.2': + resolution: {integrity: sha512-DY7pu12+LDRn6cxDMvsy1/quaPTxicAPz/kfODV7KBf8+Hq4rFWeJ4KS6m22IKIbQxrBQgmQG0WFJLaPeY7cPw==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-github@2.9.2': + resolution: {integrity: sha512-pKR94swzWxqRFUEZJBQ8WHcw6CklxLWhDyjvGpxiieAlwUAL0mlmtCcctRgsmJRuAKQlZx4WNslRgNX5aVcoZw==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-hue@2.9.2': + resolution: {integrity: sha512-vDGN5YCzw09BfxkQeDvAeQ7zAy141uJ3HkFk1lsXL7ha8xZ35AItE1s/C6d60vFjGdoloKShh0yA7df3pnjmxA==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-material@2.9.2': + resolution: {integrity: sha512-R9MI9IlTof/L1rdxUFQAWgAgUSNJGXQsPujo8UGpwR7o5d+A3wwybUnPBsGKRnZwDy5zW7x+lPxY46GXE9aU9Q==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-name@2.9.2': + resolution: {integrity: sha512-knqRUkAe3pv6rB+tGzaURtwQkBqjRG62YNlzUx8Ty7g+pskWpLSPiMikW+9H5sLPq7wU3ichZiygqIp4BRgQzA==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + + '@uiw/react-color-saturation@2.9.2': + resolution: {integrity: sha512-w1aUU+g6Axwbr1nLvF8k/zg5v7UW8z80eH6C7w+tdiOFOQKkKQlXqeOG0IIUUIj3v/ji+yM90IuOH+Ku7zsJrg==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-shade-slider@2.9.2': + resolution: {integrity: sha512-VE59rWv5ixqCN2CTpoe33j4SOCGU62bKguizx4HxgKczE/X0ySeEas8iP5XLg/4fYWl3EZN4uI+M8mNRnB0DPw==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-sketch@2.9.2': + resolution: {integrity: sha512-PEvwaqDVDdBI/+fWASBWQOvx3ows7dIcv6Z06VHgEXk2chi95Fkrbd0YUUXMcp7ESsmUK1j5ozGMLAf9Nvx6Nw==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-slider@2.9.2': + resolution: {integrity: sha512-vsi0AwmFJpb+flF8XCkacbX+MwLGOzrDKMqR29XE5sO8ERaezoT5mmYXzXXFcjyZYIuXse4C3JT38nsmOBp1vQ==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-swatch@2.9.2': + resolution: {integrity: sha512-6zBy+E9NzZR672M2wPsbbNRqKy9Wi9jOuuxxyzov1CEZp+pPX7UwMlCX6RUhKdO0PzTSPCVQmbz5bplu5vsW0w==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color-wheel@2.9.2': + resolution: {integrity: sha512-ayGzQyMZM3Cp+sX7LNElQ/QQWMO7YG4k/RQwVJAhxNQ+4lJ/p4LLSnI85D7NxILkk+jiXnjxRroxxZ2eJhWo+g==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-color@2.9.2': + resolution: {integrity: sha512-nIrw4Ol6boAkr1CUcCAkrUFVrKbT9T7/0qaSDpXmiDgKYf77gbXTWTsqVuXVDCSoCn28LvurpASS4AW8oiSBtg==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@uiw/react-drag-event-interactive@2.9.2': + resolution: {integrity: sha512-6gxQz+Ij7JkXlEOpfZhOu+Gdp/sI9VnMeDl8AJeYl3+0YXP31lXGmyb0NkNYnoUmJO+RrAf68c1raMpaDWs+Ow==} + peerDependencies: + '@babel/runtime': '>=7.19.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -4055,6 +4414,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -4384,6 +4747,14 @@ packages: colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + colors-named-hex@1.0.3: + resolution: {integrity: sha512-vhUoMdCdOKgD9Ni3p6uV3ET1JJCHzlcK6lN3/yl+6TUHinDE6HUFlmnvkh/NDZ2M9049Ipn3mX85qu6akRiC1g==} + engines: {node: '>=14.16'} + + colors-named@1.0.4: + resolution: {integrity: sha512-R254qrKSxFJNa7QmM7vrRaz5Hygr7MIaNbXcIx7WfmlYJ9OjZQ+aczGlnKS8lLtNT0GM9aGZ4EcmNXrh5ttv6g==} + engines: {node: '>=14.16'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -4795,8 +5166,8 @@ packages: babel-plugin-macros: optional: true - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -4840,6 +5211,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -5396,8 +5770,8 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.3.2: - resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} fs.realpath@1.0.0: @@ -5450,6 +5824,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -7105,11 +7483,41 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-split-grid@1.0.4: resolution: {integrity: sha512-RMEzFFnntgn+u5GFq+ognmv30CsTgrR97zB4RkMx2VSX9Sw8A7fjzCUfw/Avj7m8GCEw19GIjoUQGOQUOOjlcA==} peerDependencies: react: '*' + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -8153,12 +8561,32 @@ packages: urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-resize-observer@9.1.0: resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} peerDependencies: react: 16.8.0 - 18 react-dom: 16.8.0 - 18 + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -8180,8 +8608,8 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true - validator@13.15.23: - resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} varint@6.0.0: @@ -10003,6 +10431,23 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@floating-ui/utils@0.2.10': {} + '@greenhat616/material-react-table@4.0.0(5aa790633df21c14482e4378d5c119ba)': dependencies: '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) @@ -10848,6 +11293,27 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.0)': dependencies: react: 19.2.0 @@ -10866,6 +11332,102 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.7 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.7 + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/rect': 1.1.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-portal@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -10876,6 +11438,16 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) @@ -10904,6 +11476,23 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/number': 1.1.1 @@ -10921,6 +11510,35 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) @@ -10971,6 +11589,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.0)': dependencies: react: 19.2.0 @@ -10983,6 +11608,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) @@ -10990,6 +11622,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + + '@radix-ui/rect@1.1.1': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} '@rollup/pluginutils@4.2.1': @@ -11080,7 +11723,7 @@ snapshots: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 11.3.2 + fs-extra: 11.3.3 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.8 @@ -12019,6 +12662,206 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@uiw/color-convert@2.9.2(@babel/runtime@7.28.4)': + dependencies: + '@babel/runtime': 7.28.4 + + '@uiw/react-color-alpha@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-drag-event-interactive': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-block@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-swatch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-chrome@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-hsla': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-rgba': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-github': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-hue': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-saturation': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-circle@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-swatch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-colorful@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-hue': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-saturation': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-compact@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-rgba': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-swatch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-editable-input-hsla@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-editable-input-rgba': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-editable-input-rgba@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-editable-input@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-github@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-swatch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-hue@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-material@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-rgba': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-name@2.9.2(@babel/runtime@7.28.4)': + dependencies: + '@babel/runtime': 7.28.4 + colors-named: 1.0.4 + colors-named-hex: 1.0.3 + + '@uiw/react-color-saturation@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-drag-event-interactive': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-shade-slider@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-sketch@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-rgba': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-hue': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-saturation': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-swatch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-slider@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-swatch@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color-wheel@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-drag-event-interactive': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-color@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@uiw/color-convert': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-alpha': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-block': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-chrome': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-circle': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-colorful': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-compact': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-hsla': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-editable-input-rgba': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-github': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-hue': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-material': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-name': 2.9.2(@babel/runtime@7.28.4) + '@uiw/react-color-saturation': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-shade-slider': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-sketch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-slider': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-swatch': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-color-wheel': 2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@uiw/react-drag-event-interactive@2.9.2(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + '@ungap/structured-clone@1.2.0': {} '@unrs/resolver-binding-android-arm-eabi@1.10.1': @@ -12270,6 +13113,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.8 @@ -12661,6 +13508,10 @@ snapshots: colorjs.io@0.5.2: {} + colors-named-hex@1.0.3: {} + + colors-named@1.0.4: {} + comma-separated-tokens@2.0.3: {} commander@11.1.0: {} @@ -13058,7 +13909,7 @@ snapshots: optionalDependencies: babel-plugin-macros: 3.1.0 - dedent@1.7.0(babel-plugin-macros@3.1.0): + dedent@1.7.1(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -13093,6 +13944,8 @@ snapshots: detect-libc@2.0.4: {} + detect-node-es@1.1.0: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -13905,7 +14758,7 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@11.3.2: + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 @@ -13979,6 +14832,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -15731,12 +16586,39 @@ snapshots: react-refresh@0.18.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.0): + dependencies: + react: 19.2.0 + react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.7 + + react-remove-scroll@2.7.2(@types/react@19.2.7)(react@19.2.0): + dependencies: + react: 19.2.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.7)(react@19.2.0) + react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.7)(react@19.2.0) + use-sidecar: 1.1.3(@types/react@19.2.7)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + react-split-grid@1.0.4(react@19.2.0): dependencies: prop-types: 15.8.1 react: 19.2.0 split-grid: 1.0.11 + react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.0): + dependencies: + get-nonce: 1.0.1 + react: 19.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.7 + react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -16984,12 +17866,27 @@ snapshots: urlpattern-polyfill@10.1.0: {} + use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.0): + dependencies: + react: 19.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.7 + use-resize-observer@9.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@juggle/resize-observer': 3.4.0 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.7 + use-sync-external-store@1.5.0(react@19.2.0): dependencies: react: 19.2.0 @@ -17006,7 +17903,7 @@ snapshots: uuid@10.0.0: {} - validator@13.15.23: {} + validator@13.15.26: {} varint@6.0.0: {} diff --git a/clash-nyanpasu/scripts/package.json b/clash-nyanpasu/scripts/package.json index 97132203bc..fcde64aa22 100644 --- a/clash-nyanpasu/scripts/package.json +++ b/clash-nyanpasu/scripts/package.json @@ -19,7 +19,7 @@ "adm-zip": "0.5.16", "colorize-template": "1.0.0", "consola": "3.4.2", - "fs-extra": "11.3.2", + "fs-extra": "11.3.3", "octokit": "5.0.5", "picocolors": "1.1.1", "tar": "7.5.2", diff --git a/clash-verge-rev/.github/ISSUE_TEMPLATE/bug_report.yml b/clash-verge-rev/.github/ISSUE_TEMPLATE/bug_report.yml index d22312b0ed..30c2a2e6f1 100644 --- a/clash-verge-rev/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/clash-verge-rev/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,11 +11,11 @@ body: ## 在提交问题之前,请确认以下事项: 1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - 2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论 - 3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索 + 2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论 + 3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索 4. 请 **务必** 查看 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本更新日志 5. 请 **务必** 尝试 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本,确定问题是否仍然存在 - 6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭 + 6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 AutoBuild 版本,否则 issue 将会被直接关闭 ## Before submitting the issue, please make sure of the following checklist: diff --git a/clash-verge-rev/.github/ISSUE_TEMPLATE/feature_request.yml b/clash-verge-rev/.github/ISSUE_TEMPLATE/feature_request.yml index aaa60d9b5d..dc5a460d93 100644 --- a/clash-verge-rev/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/clash-verge-rev/.github/ISSUE_TEMPLATE/feature_request.yml @@ -10,10 +10,10 @@ body: value: | ## 在提交问题之前,请确认以下事项: 1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能 - 2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论 + 2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论 3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索 4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现 - 5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭 + 5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭 ## Before submitting the issue, please make sure of the following checklist: 1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions 2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue diff --git a/clash-verge-rev/.github/workflows/alpha.yml b/clash-verge-rev/.github/workflows/alpha.yml index cec7440ae7..425a663995 100644 --- a/clash-verge-rev/.github/workflows/alpha.yml +++ b/clash-verge-rev/.github/workflows/alpha.yml @@ -307,7 +307,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -377,7 +377,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - name: Install pnpm uses: pnpm/action-setup@v4 @@ -505,7 +505,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -522,8 +522,8 @@ jobs: - name: Download WebView2 Runtime run: | - invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab - Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri + invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab + Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri Remove-Item .\src-tauri\tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json diff --git a/clash-verge-rev/.github/workflows/autobuild.yml b/clash-verge-rev/.github/workflows/autobuild.yml index fd43e28b1b..aa983dc65d 100644 --- a/clash-verge-rev/.github/workflows/autobuild.yml +++ b/clash-verge-rev/.github/workflows/autobuild.yml @@ -46,7 +46,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -187,11 +187,11 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" cache: "pnpm" - name: Pnpm Cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.pnpm-store key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" @@ -220,12 +220,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - # APPLE_ID: ${{ secrets.APPLE_ID }} - # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: tagName: ${{ env.TAG_NAME }} releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" @@ -283,11 +283,11 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" cache: "pnpm" - name: Pnpm Cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.pnpm-store key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" @@ -433,11 +433,11 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" cache: "pnpm" - name: Pnpm Cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.pnpm-store key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" @@ -454,8 +454,8 @@ jobs: - name: Download WebView2 Runtime run: | - invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab - Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri + invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab + Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri Remove-Item .\src-tauri\tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json @@ -535,7 +535,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4.2.0 name: Install pnpm diff --git a/clash-verge-rev/.github/workflows/cross_check.yaml b/clash-verge-rev/.github/workflows/cross_check.yaml index 06eeb91551..af9651a39e 100644 --- a/clash-verge-rev/.github/workflows/cross_check.yaml +++ b/clash-verge-rev/.github/workflows/cross_check.yaml @@ -43,7 +43,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-verge-rev/.github/workflows/dev.yml b/clash-verge-rev/.github/workflows/dev.yml index 74c7dd01f6..cbf96f9b51 100644 --- a/clash-verge-rev/.github/workflows/dev.yml +++ b/clash-verge-rev/.github/workflows/dev.yml @@ -103,11 +103,11 @@ jobs: if: github.event.inputs[matrix.input] == 'true' uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" cache: "pnpm" - name: Pnpm Cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.pnpm-store key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" @@ -141,19 +141,19 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - # APPLE_ID: ${{ secrets.APPLE_ID }} - # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: tauriScript: pnpm args: --target ${{ matrix.target }} -b ${{ matrix.bundle }} - name: Upload Artifacts (macOS) if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.target }} path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg @@ -161,7 +161,7 @@ jobs: - name: Upload Artifacts (Windows) if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.target }} path: target/${{ matrix.target }}/release/bundle/nsis/*.exe @@ -169,7 +169,7 @@ jobs: - name: Upload Artifacts (Linux) if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.target }} path: target/${{ matrix.target }}/release/bundle/deb/*.deb diff --git a/clash-verge-rev/.github/workflows/frontend-check.yml b/clash-verge-rev/.github/workflows/frontend-check.yml index 4bb8455bdf..4649ce87d4 100644 --- a/clash-verge-rev/.github/workflows/frontend-check.yml +++ b/clash-verge-rev/.github/workflows/frontend-check.yml @@ -47,12 +47,12 @@ jobs: - uses: actions/setup-node@v6 if: steps.check_frontend.outputs.frontend == 'true' with: - node-version: "24.11.1" + node-version: "24.12.0" cache: "pnpm" - name: Restore pnpm cache if: steps.check_frontend.outputs.frontend == 'true' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.pnpm-store key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}" diff --git a/clash-verge-rev/.github/workflows/release.yml b/clash-verge-rev/.github/workflows/release.yml index f3f2b271fc..4fd48c8b48 100644 --- a/clash-verge-rev/.github/workflows/release.yml +++ b/clash-verge-rev/.github/workflows/release.yml @@ -121,6 +121,7 @@ jobs: ### 稳定机场VPN推荐 - [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6) + Created at ${{ env.BUILDTIME }}. EOF @@ -159,7 +160,10 @@ jobs: uses: actions/checkout@v6 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -167,8 +171,13 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - save-if: false + save-if: ${{ github.ref == 'refs/heads/dev' }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target + cache-all-crates: true + cache-workspace-crates: true - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -188,7 +197,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -200,6 +209,13 @@ jobs: pnpm i pnpm run prebuild ${{ matrix.target }} + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本 uses: tauri-apps/tauri-action@v0.6.0 @@ -243,7 +259,10 @@ jobs: uses: actions/checkout@v6 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -251,13 +270,18 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - save-if: false + save-if: ${{ github.ref == 'refs/heads/dev' }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target + cache-all-crates: true + cache-workspace-crates: true - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - name: Install pnpm uses: pnpm/action-setup@v4 @@ -313,6 +337,13 @@ jobs: gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Build for Linux run: | export PKG_CONFIG_ALLOW_CROSS=1 @@ -345,8 +376,8 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} prerelease: ${{ contains(github.ref_name, '-rc') }} files: | - src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb - src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm + target/${{ matrix.target }}/release/bundle/deb/*.deb + target/${{ matrix.target }}/release/bundle/rpm/*.rpm release-for-fixed-webview2: name: Release Build for Fixed WebView2 @@ -366,19 +397,30 @@ jobs: - name: Checkout Repository uses: actions/checkout@v6 + - name: Install Rust Stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} + - name: Add Rust Target run: rustup target add ${{ matrix.target }} - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - save-if: false + save-if: ${{ github.ref == 'refs/heads/dev' }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target + cache-all-crates: true + cache-workspace-crates: true - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -392,14 +434,20 @@ jobs: - name: Download WebView2 Runtime run: | - invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab - Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri + invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab + Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri Remove-Item .\src-tauri\tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build id: build - # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本 uses: tauri-apps/tauri-action@v0.6.0 env: NODE_OPTIONS: "--max_old_space_size=4096" @@ -438,7 +486,7 @@ jobs: body: "See release notes for detailed changelog." token: ${{ secrets.GITHUB_TOKEN }} prerelease: ${{ contains(github.ref_name, '-rc') }} - files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* + files: target/${{ matrix.target }}/release/bundle/nsis/*setup* - name: Portable Bundle run: pnpm portable-fixed-webview2 ${{ matrix.target }} @@ -457,7 +505,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -483,7 +531,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -545,7 +593,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -599,6 +647,7 @@ jobs: ### 稳定机场VPN推荐 - [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6) + Created at ${{ env.BUILDTIME }}. EOF diff --git a/clash-verge-rev/.github/workflows/updater.yml b/clash-verge-rev/.github/workflows/updater.yml index 1e182197cb..a2fdc8611c 100644 --- a/clash-verge-rev/.github/workflows/updater.yml +++ b/clash-verge-rev/.github/workflows/updater.yml @@ -15,7 +15,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -39,7 +39,7 @@ jobs: - name: Install Node uses: actions/setup-node@v6 with: - node-version: "24.11.1" + node-version: "24.12.0" - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-verge-rev/.husky/pre-commit b/clash-verge-rev/.husky/pre-commit index 4c21a04538..ea74c92fcd 100755 --- a/clash-verge-rev/.husky/pre-commit +++ b/clash-verge-rev/.husky/pre-commit @@ -28,24 +28,18 @@ pnpm exec lint-staged RUST_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src-tauri/.*\.rs$' || true)" if [ -n "$RUST_FILES" ]; then echo "[pre-commit] Formatting Rust changes with cargo fmt..." - ( - cd src-tauri - cargo fmt - ) + cargo fmt while IFS= read -r file; do [ -n "$file" ] && git add "$file" done <<< "$RUST_FILES" echo "[pre-commit] Linting Rust changes with cargo clippy..." - ( - cd src-tauri - cargo clippy-all - if ! command -v clash-verge-logging-check >/dev/null 2>&1; then - echo "[pre-commit] Installing clash-verge-logging-check..." - cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git - fi - clash-verge-logging-check - ) + cargo clippy-all + if ! command -v clash-verge-logging-check >/dev/null 2>&1; then + echo "[pre-commit] Installing clash-verge-logging-check..." + cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git + fi + clash-verge-logging-check fi TS_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx)$' || true)" diff --git a/clash-verge-rev/.husky/pre-push b/clash-verge-rev/.husky/pre-push index 4f2a6a6b70..e1781c43ff 100644 --- a/clash-verge-rev/.husky/pre-push +++ b/clash-verge-rev/.husky/pre-push @@ -25,16 +25,10 @@ pnpm typecheck if command -v cargo >/dev/null 2>&1; then echo "[pre-push] Verifying Rust formatting..." - ( - cd src-tauri - cargo fmt --check - ) + cargo fmt --check echo "[pre-push] Running cargo clippy..." - ( - cd src-tauri - cargo clippy-all - ) + cargo clippy-all else echo "[pre-push] ⚠️ cargo not found; skipping Rust checks." fi diff --git a/clash-verge-rev/CONTRIBUTING.md b/clash-verge-rev/CONTRIBUTING.md index 833289ad41..9b9b2d32b9 100644 --- a/clash-verge-rev/CONTRIBUTING.md +++ b/clash-verge-rev/CONTRIBUTING.md @@ -119,13 +119,17 @@ cargo fmt pnpm format ``` +### Signing your commit + +Signed commits are required to verify authorship and ensure your contributions can be merged. Reference signing-commits [here](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). + ### Submitting Your Changes 1. Fork the repository. 2. Create a new branch for your feature or bug fix. -3. Commit your changes with clear messages. +3. Commit your changes with clear messages and make sure it's signed. 4. Push your branch and submit a pull request. diff --git a/clash-verge-rev/Cargo.lock b/clash-verge-rev/Cargo.lock index cc3352a4dc..865975717b 100644 --- a/clash-verge-rev/Cargo.lock +++ b/clash-verge-rev/Cargo.lock @@ -588,7 +588,7 @@ dependencies = [ "axum-core 0.5.5", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "itoa", @@ -629,7 +629,7 @@ checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -1002,9 +1002,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -1059,9 +1059,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.47" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -1186,7 +1186,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clash-verge" -version = "2.4.4" +version = "2.4.4-rc.1" dependencies = [ "aes-gcm", "anyhow", @@ -1285,10 +1285,7 @@ version = "0.1.0" dependencies = [ "clash-verge-logging", "log", - "signal-hook 0.3.18", - "tauri", "tokio", - "windows-sys 0.61.2", ] [[package]] @@ -1302,8 +1299,8 @@ dependencies = [ [[package]] name = "clash_verge_logger" -version = "0.2.1" -source = "git+https://github.com/clash-verge-rev/clash-verge-logger#955f1b709890640ff01fd30009df0f35816bbca6" +version = "0.2.2" +source = "git+https://github.com/clash-verge-rev/clash-verge-logger#e4768e3852c4868ed86e7210df82c1178467820d" dependencies = [ "arraydeque", "compact_str", @@ -1315,8 +1312,8 @@ dependencies = [ [[package]] name = "clash_verge_service_ipc" -version = "2.0.21" -source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#da00a684c2b9723d647ed4992714eb669fcbd8a2" +version = "2.0.26" +source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#37b9964a9bce767b5b95ea2be75613b23400c9f0" dependencies = [ "anyhow", "compact_str", @@ -1602,9 +1599,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -2137,9 +2134,9 @@ dependencies = [ [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -2149,9 +2146,9 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", @@ -2287,9 +2284,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -3178,7 +3175,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http 1.4.0", "indexmap 2.12.1", "slab", "tokio", @@ -3267,7 +3264,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.3.1", + "http 1.4.0", "httpdate", "mime", "sha1", @@ -3279,7 +3276,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -3355,12 +3352,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -3382,7 +3378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -3393,7 +3389,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -3463,7 +3459,7 @@ dependencies = [ "futures-channel", "futures-core", "h2 0.4.12", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", @@ -3481,7 +3477,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", + "http 1.4.0", "hyper 1.8.1", "hyper-util", "rustls", @@ -3535,16 +3531,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -3996,9 +3992,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -4039,13 +4035,13 @@ dependencies = [ [[package]] name = "kode-bridge" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b158d8b9ab9f07c892a3f1b5d44a79e6c74e97541bf66488025796d429cf7aac" +checksum = "0ba2fd0531052f4438d171509ee7686b6b2207314366a5e2dde80586f6d6a09b" dependencies = [ "bytes", "futures", - "http 1.3.1", + "http 1.4.0", "httparse", "interprocess", "libc", @@ -4161,9 +4157,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c" dependencies = [ "zlib-rs", ] @@ -4274,9 +4270,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee70bb2bba058d58e252d2944582d634fc884fc9c489a966d428dedcf653e97" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" dependencies = [ "cc", "objc2 0.6.3", @@ -4425,9 +4421,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -4436,9 +4432,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -4817,7 +4813,7 @@ dependencies = [ "objc2-core-text", "objc2-core-video", "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", + "objc2-quartz-core", ] [[package]] @@ -4972,18 +4968,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-osa-kit" version = "0.3.2" @@ -4996,19 +4980,6 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -5017,6 +4988,7 @@ checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.10.0", "objc2 0.6.3", + "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -5780,7 +5752,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.23.9", ] [[package]] @@ -5924,9 +5896,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -6274,9 +6246,9 @@ checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64 0.22.1", "bytes", @@ -6286,7 +6258,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.12", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -6311,7 +6283,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", @@ -6330,7 +6302,7 @@ dependencies = [ "async-trait", "chrono", "digest_auth", - "http 1.3.1", + "http 1.4.0", "httpdate", "reqwest", "serde", @@ -6589,9 +6561,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -6912,9 +6884,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", @@ -6931,9 +6903,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling", "proc-macro2", @@ -7112,9 +7084,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" @@ -7210,24 +7182,24 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types 0.5.0", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core", "raw-window-handle", "redox_syscall", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7413,8 +7385,8 @@ dependencies = [ [[package]] name = "sysproxy" -version = "0.4.0" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#0f844dd2639b0ac74da4548b1325335844947420" +version = "0.4.2" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#51256d5921a01bbcf24fe5629296d2bfe493329d" dependencies = [ "interfaces", "iptools", @@ -7543,9 +7515,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.4" +version = "2.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15524fc7959bfcaa051ba6d0b3fb1ef18e978de2176c7c6acb977f7fd14d35c7" +checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" dependencies = [ "anyhow", "bytes", @@ -7557,7 +7529,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http 1.3.1", + "http 1.4.0", "http-range", "image", "jni", @@ -7660,9 +7632,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" dependencies = [ "anyhow", "glob", @@ -7828,7 +7800,7 @@ dependencies = [ "bytes", "cookie_store", "data-url", - "http 1.3.1", + "http 1.4.0", "regex", "reqwest", "schemars 0.8.22", @@ -7846,11 +7818,11 @@ dependencies = [ [[package]] name = "tauri-plugin-mihomo" version = "0.1.1" -source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#24586eb0721314f88e65460b4ac01933b3376d3c" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#153f16f7b3f979aa130a2d3d7c39a52a39987288" dependencies = [ "base64 0.22.1", "futures-util", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "pin-project", @@ -7928,7 +7900,7 @@ dependencies = [ "dirs 6.0.0", "flate2", "futures-util", - "http 1.3.1", + "http 1.4.0", "infer", "log", "minisign-verify", @@ -7974,7 +7946,7 @@ dependencies = [ "cookie", "dpi", "gtk", - "http 1.3.1", + "http 1.4.0", "jni", "objc2 0.6.3", "objc2-ui-kit", @@ -7992,12 +7964,12 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7950f3bde6bcca6655bc5e76d3d6ec587ceb81032851ab4ddbe1f508bdea2729" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" dependencies = [ "gtk", - "http 1.3.1", + "http 1.4.0", "jni", "log", "objc2 0.6.3", @@ -8031,7 +8003,7 @@ dependencies = [ "dunce", "glob", "html5ever", - "http 1.3.1", + "http 1.4.0", "infer", "json-patch", "kuchikiki", @@ -8444,7 +8416,7 @@ dependencies = [ "toml_datetime 0.7.3", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -8498,19 +8470,19 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -8519,7 +8491,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -8572,7 +8544,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2 0.4.12", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -8693,14 +8665,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "iri-string", "pin-project-lite", @@ -8723,9 +8695,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -8735,9 +8707,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -8746,9 +8718,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -8767,9 +8739,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -8863,7 +8835,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.9.2", @@ -9044,13 +9016,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -9147,7 +9119,7 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -9190,9 +9162,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -9203,9 +9175,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -9216,9 +9188,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9226,9 +9198,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -9239,9 +9211,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -9334,9 +9306,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -10049,9 +10021,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -10093,15 +10065,14 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wl-clipboard-rs" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" dependencies = [ "libc", "log", "os_pipe", - "rustix 0.38.44", - "tempfile", + "rustix 1.1.2", "thiserror 2.0.17", "tree_magic_mini", "wayland-backend", @@ -10138,7 +10109,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http 1.3.1", + "http 1.4.0", "javascriptcore-rs", "jni", "kuchikiki", @@ -10292,7 +10263,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.13", + "winnow 0.7.14", "zbus_macros", "zbus_names", "zvariant", @@ -10321,24 +10292,24 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.13", + "winnow 0.7.14", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -10461,9 +10432,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235" [[package]] name = "zopfli" @@ -10530,7 +10501,7 @@ dependencies = [ "enumflags2", "serde", "url", - "winnow 0.7.13", + "winnow 0.7.14", "zvariant_derive", "zvariant_utils", ] @@ -10558,5 +10529,5 @@ dependencies = [ "quote", "serde", "syn 2.0.111", - "winnow 0.7.13", + "winnow 0.7.14", ] diff --git a/clash-verge-rev/Cargo.toml b/clash-verge-rev/Cargo.toml index dd4607e89b..77c5657f19 100644 --- a/clash-verge-rev/Cargo.toml +++ b/clash-verge-rev/Cargo.toml @@ -9,9 +9,6 @@ members = [ ] resolver = "2" -[workspace.package] -edition = "2024" -rust-version = "1.91" [profile.release] panic = "abort" @@ -49,7 +46,7 @@ clash-verge-signal = { path = "crates/clash-verge-signal" } clash-verge-types = { path = "crates/clash-verge-types" } tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" } -tauri = { version = "2.9.4" } +tauri = { version = "2.9.5" } tauri-plugin-clipboard-manager = "2.3.2" parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } anyhow = "1.0.100" diff --git a/clash-verge-rev/Changelog.md b/clash-verge-rev/Changelog.md index f786ab6ef2..46dbd71b35 100644 --- a/clash-verge-rev/Changelog.md +++ b/clash-verge-rev/Changelog.md @@ -2,10 +2,6 @@ - **Mihomo(Meta) 内核升级至 v1.19.17** -> [!WARNING] -> Apple 公证服务故障,临时暂停 macOS 签名 -> macOS 跳过签名,终端执行 `sudo xattr -rd com.apple.quarantine /Applications/Clash\ Verge.app/` - ### 🐞 修复问题 - Linux 无法切换 TUN 堆栈 @@ -17,7 +13,6 @@ - Monaco 编辑器的行数上限 - 已删除节点在手动分组中导致配置无法加载 - 仪表盘与托盘状态不同步 -- 修复重启或退出应用,关闭系统时无法记忆用户行为 - 彻底修复 macOS 连接页面显示异常 - windows 端监听关机信号失败 - 修复代理按钮和高亮状态不同步 @@ -27,6 +22,14 @@ - 修复在搜索框输入不完整正则直接崩溃 - 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁 - 修复更新时加载进度条异常 +- 升级内核失败导致内核不可用问题 +- 修复 macOS 在安装和卸载服务时提示与操作不匹配 +- 修复菜单排序模式拖拽异常 +- 修复托盘菜单代理组前的异常勾选状态 +- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留 +- 修复直接覆盖 `config.yaml` 使用时无法展开代理组 +- 修复 macOS 下应用启动时系统托盘图标颜色闪烁 +- 修复应用静默启动模式下非全局热键一直抢占其他应用按键问题
✨ 新增功能 @@ -36,6 +39,9 @@ - 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接) - 日志页面支持按时间倒序 - 增加「重新激活订阅」的全局快捷键 +- WebView2 Runtime 修复构建升级到 133.0.3065.92 +- 侧边栏右键新增「恢复默认排序」 +- Linux 下新增对 TUN 「自动重定向」(`auto-redirect` 字段)的配置支持,默认关闭
@@ -47,7 +53,7 @@ - 替换前端信息编辑组件,提供更好性能 - 优化后端内存和性能表现 - 防止退出时可能的禁用 TUN 失败 -- i18n 支持 +- 全新 i18n 支持方式 - 优化备份设置布局 - 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停 - 性能优化系统状态获取 @@ -58,6 +64,11 @@ - 优化前端数据刷新 - 优化流量采样和数据处理 - 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间 +- 优化前端 WebSocket 连接机制 +- 改进旧版 Service 需要重新安装检测流程 +- 优化 macOS, Linux 和 Windows 系统信号处理 +- 链式代理仅显示 Selector 类型规则组 +- 优化 Windows 系统代理设置,不再依赖 `sysproxy.exe` 来设置代理 diff --git a/clash-verge-rev/README.md b/clash-verge-rev/README.md index b7e70f7dfe..761dce423e 100644 --- a/clash-verge-rev/README.md +++ b/clash-verge-rev/README.md @@ -16,7 +16,8 @@ A Clash Meta GUI based on TauriEspañol · Русский · 日本語 · - 한국어 + 한국어 · + فارسی

## Preview @@ -47,17 +48,23 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple). ## Promotion -#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6) +### ✈️ [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6) -- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。 -- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6) -- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份) -- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折 -- 海外团队,无跑路风险,高达 50% 返佣 -- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开 -- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配) -- 解锁流媒体及 ChatGPT -- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) +🚀 高性能海外技术流机场,支持免费试用与优惠套餐,全面解锁流媒体及 AI 服务,全球首家采用 **QUIC 协议**。 + +🎁 使用 **Clash Verge 专属邀请链接** 注册即送 **3 天免费试用**,每日 **1GB 流量**:👉 [点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6) + +#### **核心优势:** + +- 📱 自研 iOS 客户端(业内"唯一")技术经得起考验,极大**持续研发**投入 +- 🧑‍💻 **12小时真人客服**(顺带解决 Clash Verge 使用问题) +- 💰 优惠套餐每月**仅需 21 元,160G 流量,年付 8 折** +- 🌍 海外团队,无跑路风险,高达 50% 返佣 +- ⚙️ **集群负载均衡**设计,**负载监控和随时扩容**,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开 +- ⚡ 全球首家**Quic 协议机场**,现已上线更快的 Tuic 协议(Clash Verge 客户端最佳搭配) +- 🎬 解锁**流媒体及 主流 AI** + +🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) #### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持, diff --git a/clash-verge-rev/crates/clash-verge-logging/src/lib.rs b/clash-verge-rev/crates/clash-verge-logging/src/lib.rs index bb8672f538..a30c15ee74 100644 --- a/clash-verge-rev/crates/clash-verge-logging/src/lib.rs +++ b/clash-verge-rev/crates/clash-verge-logging/src/lib.rs @@ -18,6 +18,7 @@ pub enum Type { Config, Setup, System, + SystemSignal, Service, Hotkey, Window, @@ -42,6 +43,7 @@ impl fmt::Display for Type { Self::Config => write!(f, "[Config]"), Self::Setup => write!(f, "[Setup]"), Self::System => write!(f, "[System]"), + Self::SystemSignal => write!(f, "[SysSignal]"), Self::Service => write!(f, "[Service]"), Self::Hotkey => write!(f, "[Hotkey]"), Self::Window => write!(f, "[Window]"), diff --git a/clash-verge-rev/crates/clash-verge-signal/Cargo.toml b/clash-verge-rev/crates/clash-verge-signal/Cargo.toml index 2781509f29..ef3b129836 100644 --- a/clash-verge-rev/crates/clash-verge-signal/Cargo.toml +++ b/clash-verge-rev/crates/clash-verge-signal/Cargo.toml @@ -1,26 +1,13 @@ [package] name = "clash-verge-signal" version = "0.1.0" -edition.workspace = true -rust-version.workspace = true +edition = "2024" +rust-version = "1.91" [dependencies] clash-verge-logging = { workspace = true } log = { workspace = true } tokio = { workspace = true } -[target.'cfg(unix)'.dependencies] -signal-hook = "0.3.18" - -[target.'cfg(windows)'.dependencies] -tauri = { workspace = true } -windows-sys = { version = "0.61.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_System_SystemServices", - "Win32_UI_WindowsAndMessaging", -] } - - [lints] workspace = true diff --git a/clash-verge-rev/crates/clash-verge-signal/src/lib.rs b/clash-verge-rev/crates/clash-verge-signal/src/lib.rs index a7b7671bc9..46b559d33e 100644 --- a/clash-verge-rev/crates/clash-verge-signal/src/lib.rs +++ b/clash-verge-rev/crates/clash-verge-signal/src/lib.rs @@ -9,7 +9,7 @@ mod windows; pub(crate) static RUNTIME: OnceLock> = OnceLock::new(); -pub fn register(#[cfg(windows)] app_handle: &tauri::AppHandle, f: F) +pub fn register(f: F) where F: Fn() -> Fut + Send + Sync + 'static, Fut: Future + Send + 'static, @@ -19,7 +19,7 @@ where Err(e) => { logging!( info, - Type::System, + Type::SystemSignal, "register shutdown signal failed, create tokio runtime error: {}", e ); @@ -31,5 +31,5 @@ where unix::register(f); #[cfg(windows)] - windows::register(app_handle, f); + windows::register(f); } diff --git a/clash-verge-rev/crates/clash-verge-signal/src/unix.rs b/clash-verge-rev/crates/clash-verge-signal/src/unix.rs index 9729c32b46..aa81393bb0 100644 --- a/clash-verge-rev/crates/clash-verge-signal/src/unix.rs +++ b/clash-verge-rev/crates/clash-verge-signal/src/unix.rs @@ -1,13 +1,12 @@ -use signal_hook::{ - consts::{SIGHUP, SIGINT, SIGTERM}, - iterator::Signals, - low_level, -}; +use std::sync::atomic::{AtomicBool, Ordering}; -use clash_verge_logging::{Type, logging, logging_error}; +use clash_verge_logging::{Type, logging}; +use tokio::signal::unix::{SignalKind, signal}; use crate::RUNTIME; +static IS_CLEANING_UP: AtomicBool = AtomicBool::new(false); + pub fn register(f: F) where F: Fn() -> Fut + Send + Sync + 'static, @@ -15,39 +14,80 @@ where { if let Some(Some(rt)) = RUNTIME.get() { rt.spawn(async move { - let signals = [SIGTERM, SIGINT, SIGHUP]; - - let mut sigs = match Signals::new(signals) { + let mut sigterm = match signal(SignalKind::terminate()) { Ok(s) => s, Err(e) => { - logging!(error, Type::System, "注册信号处理器失败: {}", e); + logging!( + error, + Type::SystemSignal, + "Failed to register SIGTERM: {}", + e + ); + return; + } + }; + let mut sigint = match signal(SignalKind::interrupt()) { + Ok(s) => s, + Err(e) => { + logging!( + error, + Type::SystemSignal, + "Failed to register SIGINT: {}", + e + ); + return; + } + }; + let mut sighup = match signal(SignalKind::hangup()) { + Ok(s) => s, + Err(e) => { + logging!( + error, + Type::SystemSignal, + "Failed to register SIGHUP: {}", + e + ); return; } }; - for signal in &mut sigs { - let signal_to_str = |signal| match signal { - SIGTERM => "SIGTERM", - SIGINT => "SIGINT", - SIGHUP => "SIGHUP", - _ => "UNKNOWN", - }; + loop { + let signal_name; + tokio::select! { + _ = sigterm.recv() => { + signal_name = "SIGTERM"; + } + _ = sigint.recv() => { + signal_name = "SIGINT"; + } + _ = sighup.recv() => { + signal_name = "SIGHUP"; + } + else => { + break; + } + } - logging!(info, Type::System, "捕获到信号 {}", signal_to_str(signal)); + if IS_CLEANING_UP.load(Ordering::SeqCst) { + logging!( + info, + Type::SystemSignal, + "Already shutting down, ignoring repeated signal: {}", + signal_name + ); + continue; + } + IS_CLEANING_UP.store(true, Ordering::SeqCst); + + logging!(info, Type::SystemSignal, "Caught signal {}", signal_name); f().await; - - logging_error!( - Type::System, - "信号 {:?} 默认处理失败", - low_level::emulate_default_handler(signal) - ); } }); } else { logging!( error, - Type::System, + Type::SystemSignal, "register shutdown signal failed, RUNTIME is not available" ); } diff --git a/clash-verge-rev/crates/clash-verge-signal/src/windows.rs b/clash-verge-rev/crates/clash-verge-signal/src/windows.rs index 860208a28f..cf64912c8a 100644 --- a/clash-verge-rev/crates/clash-verge-signal/src/windows.rs +++ b/clash-verge-rev/crates/clash-verge-signal/src/windows.rs @@ -1,161 +1,114 @@ -use std::{future::Future, pin::Pin, sync::OnceLock}; - -use tauri::{AppHandle, Manager as _}; -use windows_sys::Win32::{ - Foundation::{HWND, LPARAM, LRESULT, WPARAM}, - UI::WindowsAndMessaging::{ - CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassW, - WM_ENDSESSION, WM_QUERYENDSESSION, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, - WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, - }, -}; +use std::sync::atomic::{AtomicBool, Ordering}; use clash_verge_logging::{Type, logging}; +use tokio::signal::windows; use crate::RUNTIME; -// code refer to: -// global-hotkey (https://github.com/tauri-apps/global-hotkey) -// Global Shortcut (https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut) +static IS_CLEANING_UP: AtomicBool = AtomicBool::new(false); -type ShutdownHandler = - Box Pin + Send>> + Send + Sync>; - -static SHUTDOWN_HANDLER: OnceLock = OnceLock::new(); - -struct ShutdownState { - hwnd: HWND, -} - -unsafe impl Send for ShutdownState {} -unsafe impl Sync for ShutdownState {} - -impl Drop for ShutdownState { - fn drop(&mut self) { - // this log not be printed, I don't know why. - logging!(info, Type::System, "正在销毁系统关闭监听窗口"); - unsafe { - DestroyWindow(self.hwnd); - } - } -} - -unsafe extern "system" fn shutdown_proc( - hwnd: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, -) -> LRESULT { - // refer: https://learn.microsoft.com/zh-cn/windows/win32/shutdown/shutting-down#shutdown-notifications - // only perform reset operations in `WM_ENDSESSION` - match msg { - WM_QUERYENDSESSION => { - logging!( - info, - Type::System, - "System is shutting down or user is logging off." - ); - } - WM_ENDSESSION => { - if let Some(handler) = SHUTDOWN_HANDLER.get() { - if let Some(Some(rt)) = RUNTIME.get() { - rt.block_on(async { - logging!(info, Type::System, "Session ended, system shutting down."); - handler().await; - logging!(info, Type::System, "resolved reset finished"); - }); - } else { - logging!( - error, - Type::System, - "handle shutdown signal failed, RUNTIME is not available" - ); - } - } else { - logging!( - error, - Type::System, - "WM_ENDSESSION received but no shutdown handler is registered" - ); - } - } - _ => {} - }; - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } -} - -fn encode_wide>(string: S) -> Vec { - std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) - .chain(std::iter::once(0)) - .collect::>() -} - -fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE { - // Gets the instance handle by taking the address of the - // pseudo-variable created by the microsoft linker: - // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 - - // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: - // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance - - unsafe extern "C" { - static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; - } - - unsafe { &__ImageBase as *const _ as _ } -} - -pub fn register(app_handle: &AppHandle, f: F) +pub fn register(f: F) where F: Fn() -> Fut + Send + Sync + 'static, Fut: Future + Send + 'static, { - let _ = SHUTDOWN_HANDLER.set(Box::new(move || { - let fut = (f)(); - Box::pin(async move { - fut.await; - }) as Pin + Send>> - })); + if let Some(Some(rt)) = RUNTIME.get() { + rt.spawn(async move { + let mut ctrl_c = match windows::ctrl_c() { + Ok(s) => s, + Err(e) => { + logging!( + error, + Type::SystemSignal, + "Failed to register Ctrl+C: {}", + e + ); + return; + } + }; - let class_name = encode_wide("global_shutdown_app"); - unsafe { - let hinstance = get_instance_handle(); + let mut ctrl_close = match windows::ctrl_close() { + Ok(s) => s, + Err(e) => { + logging!( + error, + Type::SystemSignal, + "Failed to register Ctrl+Close: {}", + e + ); + return; + } + }; - let wnd_class = WNDCLASSW { - lpfnWndProc: Some(shutdown_proc), - lpszClassName: class_name.as_ptr(), - hInstance: hinstance, - ..std::mem::zeroed() - }; + let mut ctrl_shutdown = match windows::ctrl_shutdown() { + Ok(s) => s, + Err(e) => { + logging!( + error, + Type::SystemSignal, + "Failed to register Ctrl+Shutdown: {}", + e + ); + return; + } + }; - RegisterClassW(&wnd_class); + let mut ctrl_logoff = match windows::ctrl_logoff() { + Ok(s) => s, + Err(e) => { + logging!( + error, + Type::SystemSignal, + "Failed to register Ctrl+Logoff: {}", + e + ); + return; + } + }; - let hwnd = CreateWindowExW( - WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | - // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which - // we want to avoid. If you remove this style, this window won't show up in the - // taskbar *initially*, but it can show up at some later point. This can sometimes - // happen on its own after several hours have passed, although this has proven - // difficult to reproduce. Alternatively, it can be manually triggered by killing - // `explorer.exe` and then starting the process back up. - // It is unclear why the bug is triggered by waiting for several hours. - WS_EX_TOOLWINDOW, - class_name.as_ptr(), - std::ptr::null(), - WS_OVERLAPPED, - CW_USEDEFAULT, - 0, - CW_USEDEFAULT, - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - hinstance, - std::ptr::null_mut(), + loop { + let signal_name; + tokio::select! { + _ = ctrl_c.recv() => { + signal_name = "Ctrl+C"; + } + _ = ctrl_close.recv() => { + signal_name = "Ctrl+Close"; + } + _ = ctrl_shutdown.recv() => { + signal_name = "Ctrl+Shutdown"; + } + _ = ctrl_logoff.recv() => { + signal_name = "Ctrl+Logoff"; + } + } + + if IS_CLEANING_UP.load(Ordering::SeqCst) { + logging!( + info, + Type::SystemSignal, + "Already shutting down, ignoring repeated signal: {}", + signal_name + ); + continue; + } + IS_CLEANING_UP.store(true, Ordering::SeqCst); + + logging!( + info, + Type::SystemSignal, + "Caught Windows signal: {}", + signal_name + ); + + f().await; + } + }); + } else { + logging!( + error, + Type::SystemSignal, + "register shutdown signal failed, RUNTIME is not available" ); - if hwnd.is_null() { - logging!(error, Type::System, "failed to create shutdown window"); - } else { - app_handle.manage(ShutdownState { hwnd }); - } } } diff --git a/clash-verge-rev/crates/clash-verge-types/Cargo.toml b/clash-verge-rev/crates/clash-verge-types/Cargo.toml index 256a2e7869..8f89c5820a 100644 --- a/clash-verge-rev/crates/clash-verge-types/Cargo.toml +++ b/clash-verge-rev/crates/clash-verge-types/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "clash-verge-types" version = "0.1.0" -edition.workspace = true -rust-version.workspace = true +edition = "2024" +rust-version = "1.91" [dependencies] serde = { workspace = true } diff --git a/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml index 2ed474147b..2d61407099 100644 --- a/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml +++ b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "tauri-plugin-clash-verge-sysinfo" version = "0.1.0" -edition.workspace = true -rust-version.workspace = true +edition = "2024" +rust-version = "1.91" [dependencies] tauri = { workspace = true } diff --git a/clash-verge-rev/docs/README_en.md b/clash-verge-rev/docs/README_en.md index ae1ad194a4..a28ad5caa7 100644 --- a/clash-verge-rev/docs/README_en.md +++ b/clash-verge-rev/docs/README_en.md @@ -16,7 +16,8 @@ A Clash Meta GUI built with Tauri< Español · Русский · 日本語 · - 한국어 + 한국어 · + فارسی

## Preview @@ -50,17 +51,23 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements. ## Promotion -#### [Doggygo VPN — Performance-oriented global accelerator](https://verge.dginv.click/#/register?code=oaxsAGo6) +### ✈️ [Doggygo VPN — A Technical-Grade Proxy Service](https://verge.dginv.click/#/register?code=oaxsAGo6) -- High-performance overseas network service with free trials, discounted plans, streaming unlocks, and first-class Hysteria protocol support. -- Register through the exclusive Clash Verge link to get a 3-day trial with 1 GB of traffic per day: [Sign up](https://verge.dginv.click/#/register?code=oaxsAGo6) -- Exclusive 20% off coupon for Clash Verge users: `verge20` (limited to 500 uses) -- Discounted bundle from ¥15.8 per month for 160 GB, plus an additional 20% off for yearly billing -- Operated by an overseas team with reliable service and up to 50% revenue share -- Load-balanced clusters with high-speed dedicated routes (compatible with legacy clients), exceptionally low latency, smooth 4K playback -- First global provider to support the `Hysteria2` protocol—perfect fit for the Clash Verge client -- Supports streaming services and ChatGPT access -- Official site: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) +🚀 A high-performance, overseas, technical-grade proxy service offering free trials and discounted plans, fully unlocking streaming platforms and AI services. The world’s first provider to adopt the **QUIC protocol**. + +🎁 Register via the **Clash Verge exclusive invitation link** to receive **3 days of free trial**, with **1GB traffic per day**: 👉 [Register here](https://verge.dginv.click/#/register?code=oaxsAGo6) + +#### **Core Advantages:** + +- 📱 Self-developed iOS client (the industry’s “only one”), with technology proven in production and **significant ongoing R&D investment** +- 🧑‍💻 **12-hour live customer support** (also assists with Clash Verge usage issues) +- 💰 Discounted plans at **only CNY 21 per month, 160GB traffic, 20% off with annual billing** +- 🌍 Overseas team, no risk of shutdown or exit scams, with up to **50% referral commission** +- ⚙️ **Cluster-based load balancing** architecture with **real-time load monitoring and elastic scaling**, high-speed dedicated lines (compatible with legacy clients), ultra-low latency, unaffected by peak hours, **4K streaming loads instantly** +- ⚡ The world’s first **QUIC-protocol-based proxy service**, now upgraded with the faster **Tuic protocol** (best paired with the Clash Verge client) +- 🎬 Unlocks **streaming platforms and mainstream AI services** + +🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) #### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827) diff --git a/clash-verge-rev/docs/README_es.md b/clash-verge-rev/docs/README_es.md index 2380bfdd26..34ce3beced 100644 --- a/clash-verge-rev/docs/README_es.md +++ b/clash-verge-rev/docs/README_es.md @@ -16,7 +16,8 @@ Una interfaz gráfica para Clash Meta construida con Español · Русский · 日本語 · - 한국어 + 한국어 · + فارسی

## Vista previa diff --git a/clash-verge-rev/docs/README_fa.md b/clash-verge-rev/docs/README_fa.md new file mode 100644 index 0000000000..280039cf31 --- /dev/null +++ b/clash-verge-rev/docs/README_fa.md @@ -0,0 +1,124 @@ +

+ Clash +
+ Continuation of Clash Verge +
+

+ +

+ یک رابط کاربری گرافیکی Clash Meta که با Tauri ساخته شده است. +

+ +

+ زبان‌ها: + 简体中文 · + English · + Español · + Русский · + 日本語 · + 한국어 · + فارسی +

+ +## پیش‌نمایش + +| تاریک | روشن | +| ----------------------------------- | ------------------------------------- | +| ![Dark Preview](./preview_dark.png) | ![Light Preview](./preview_light.png) | + +## نصب + +برای دانلود فایل نصبی متناسب با پلتفرم خود، به [صفحه انتشار](https://github.com/clash-verge-rev/clash-verge-rev/releases) مراجعه کنید.
ما بسته‌هایی برای ویندوز (x64/x86)، لینوکس (x64/arm64) و macOS 10.15+ (اینتل/اپل) ارائه می‌دهیم. + +#### انتخاب کانال انتشار + +| Channel | توضیحات | Link | +| :---------- | :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- | +| Stable | ساخت رسمی با قابلیت اطمینان بالا، ایده‌آل برای استفاده روزانه. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | +| Alpha (EOL) | نسخه‌های قدیمی (Legacy builds) برای اعتبارسنجی خط لوله انتشار (publish pipeline) استفاده می‌شوند. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) | +| AutoBuild | نسخه‌های آزمایشی برای آزمایش و دریافت بازخورد. منتظر تغییرات آزمایشی باشید. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) | + +#### راهنماهای نصب و سوالات متداول + +برای مراحل نصب، عیب‌یابی و سوالات متداول، [مستندات پروژه](https://clash-verge-rev.github.io/) را مطالعه کنید. + +--- + +### کانال تلگرام + +برای اطلاع از آخرین اخبار به [@clash_verge_rev](https://t.me/clash_verge_re) بپیوندید. + +## تبلیغات + +#### [Doggygo VPN — شتاب‌دهنده جهانی عملکردگرا](https://verge.dginv.click/#/register?code=oaxsAGo6) + +- سرویس شبکه برون مرزی با عملکرد بالا به همراه دوره‌های آزمایشی رایگان، طرح‌های تخفیف‌دار، امکان باز کردن قفل استریم و پشتیبانی درجه یک از پروتکل هیستریا. +- از طریق لینک اختصاصی Clash Verge ثبت نام کنید تا یک دوره آزمایشی ۳ روزه با ۱ گیگابایت ترافیک در روز دریافت کنید: [ثبت نام](https://verge.dginv.click/#/register?code=oaxsAGo6) +- کوپن تخفیف ۲۰٪ ویژه کاربران Clash Verge: `verge20` (محدود به ۵۰۰ بار استفاده) +- بسته تخفیف‌دار از ۱۵.۸ ین در ماه برای ۱۶۰ گیگابایت، به علاوه ۲۰٪ تخفیف اضافی برای صورتحساب سالانه +- توسط یک تیم خارجی با خدمات قابل اعتماد و تا 50٪ سهم درآمد اداره می‌شود +- کلاسترهای متعادل بار با مسیرهای اختصاصی پرسرعت (سازگار با کلاینت‌های قدیمی)، تأخیر فوق‌العاده کم، پخش روان 4K +- اولین ارائه‌دهنده جهانی که از پروتکل «Hysteria2» پشتیبانی می‌کند - کاملاً مناسب برای کلاینت Clash Verge +- پشتیبانی از سرویس‌های استریم و دسترسی به ChatGPT +- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) + +#### حامی زیرساخت ساخت — [سرورهای اختصاصی YXVM](https://yxvm.com/aff.php?aff=827) + +بیلدها و نسخه‌های ما روی سرورهای اختصاصی YXVM اجرا می‌شوند که منابع ممتاز، عملکرد قوی و شبکه پرسرعت را ارائه می‌دهند. اگر دانلودها سریع و استفاده از آن سریع به نظر می‌رسد، به لطف سخت‌افزار قوی است. +🧩 نکات برجسته سرورهای اختصاصی YXVM: + +- 🌎 مسیرهای جهانی بهینه شده برای دانلودهای بسیار سریعتر +- 🔧 منابع فیزیکی به جای ظرفیت VPS مشترک برای حداکثر کارایی +- 🧠 عالی برای بارهای کاری پروکسی، میزبانی سرویس‌های وب/CDN، خطوط لوله CI/CD یا هرگونه کار با بار بالا +- 💡 آماده استفاده فوری با گزینه‌های متعدد مرکز داده، از جمله CN2 و IEPL +- 📦 پیکربندی مورد استفاده در این پروژه در حال فروش است - می‌توانید همان تنظیمات را تهیه کنید. +- 🎯 آیا محیط ساخت مشابهی می‌خواهید؟ [همین امروز یک سرور YXVM سفارش دهید](https://yxvm.com/aff.php?aff=827) + +## ویژگی‌ها + +- ساخته شده بر اساس Rust با کارایی بالا و فریم‌ورک Tauri 2 +- با هسته جاسازی‌شده [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) ارائه می‌شود و از تغییر به کانال «آلفا» پشتیبانی می‌کند. +- رابط کاربری تمیز و مرتب با کنترل‌های رنگ تم، آیکون‌های گروه/سینی پروکسی و `تزریق CSS` +- مدیریت پروفایل پیشرفته (ادغام و کمک‌کننده‌های اسکریپت) با نکات مربوط به سینتکس پیکربندی +- کنترل‌های پروکسی سیستم، حالت محافظت و پشتیبانی از `TUN` (آداپتور شبکه مجازی) +- ویرایشگرهای بصری برای گره‌ها و قوانین +- پشتیبان‌گیری و همگام‌سازی مبتنی بر WebDAV برای تنظیمات + +### سوالات متداول + +برای راهنمایی‌های مربوط به هر پلتفرم، به [صفحه سوالات متداول](https://clash-verge-rev.github.io/faq/windows.html) مراجعه کنید. + +### اهدا + +[پشتیبانی از توسعه Clash Verge Rev](https://github.com/sponsors/clash-verge-rev) + +## توسعه + +برای دستورالعمل‌های دقیق مشارکت، به [CONTRIBUTING.md](../CONTRIBUTING.md) مراجعه کنید. + +پس از نصب تمام پیش‌نیازهای **Tauri**، پوسته توسعه را با دستور زیر اجرا کنید: + +```shell +pnpm i +pnpm run prebuild +pnpm dev +``` + +## مشارکت‌ها + +مشکلات و درخواست‌های pull مورد استقبال قرار می‌گیرند! + +## تقدیر و تشکر + +Clash Verge Rev بر اساس این پروژه‌ها ساخته شده یا از آنها الهام گرفته است: + +- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): یک رابط کاربری گرافیکی Clash مبتنی بر Tauri برای ویندوز، macOS و لینوکس.. +- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): ساخت برنامه‌های دسکتاپ کوچک‌تر، سریع‌تر و امن‌تر با رابط کاربری وب. +- [Dreamacro/clash](https://github.com/Dreamacro/clash): یک تونل مبتنی بر قانون که با زبان Go نوشته شده است. +- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): یک تونل مبتنی بر قانون که با زبان Go نوشته شده است. +- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): رابط کاربری گرافیکی Clash برای ویندوز و macOS. +- [vitejs/vite](https://github.com/vitejs/vite): ابزارهای فرانت‌اند نسل بعدی با DX فوق‌العاده سریع. + +## مجوز + +مجوز GPL-3.0. برای جزئیات بیشتر به [فایل مجوز](../LICENSE) مراجعه کنید. diff --git a/clash-verge-rev/docs/README_ja.md b/clash-verge-rev/docs/README_ja.md index b74b62d4c8..65aa45e64f 100644 --- a/clash-verge-rev/docs/README_ja.md +++ b/clash-verge-rev/docs/README_ja.md @@ -16,7 +16,8 @@ Español · Русский · 日本語 · - 한국어 + 한국어 · + فارسی

## プレビュー diff --git a/clash-verge-rev/docs/README_ko.md b/clash-verge-rev/docs/README_ko.md index 997586ae72..5771702d79 100644 --- a/clash-verge-rev/docs/README_ko.md +++ b/clash-verge-rev/docs/README_ko.md @@ -16,7 +16,8 @@ Español · Русский · 日本語 · - 한국어 + 한국어 · + فارسی

## 미리보기 diff --git a/clash-verge-rev/docs/README_ru.md b/clash-verge-rev/docs/README_ru.md index 6bbc3cbb76..9dbead4458 100644 --- a/clash-verge-rev/docs/README_ru.md +++ b/clash-verge-rev/docs/README_ru.md @@ -16,7 +16,8 @@ Clash Meta GUI базируется на Español · Русский · 日本語 · - 한국어 + 한국어 · + فارسی

## Предпросмотр diff --git a/clash-verge-rev/package.json b/clash-verge-rev/package.json index fcc08e072f..cfa4513061 100644 --- a/clash-verge-rev/package.json +++ b/clash-verge-rev/package.json @@ -1,6 +1,6 @@ { "name": "clash-verge", - "version": "2.4.4", + "version": "2.4.4-rc.1", "license": "GPL-3.0-only", "scripts": { "prepare": "husky || true", @@ -40,11 +40,11 @@ "@emotion/styled": "^11.14.1", "@juggle/resize-observer": "^3.4.0", "@monaco-editor/react": "^4.7.0", - "@mui/icons-material": "^7.3.5", + "@mui/icons-material": "^7.3.6", "@mui/lab": "7.0.0-beta.17", - "@mui/material": "^7.3.5", + "@mui/material": "^7.3.6", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", + "@tanstack/react-virtual": "^3.13.13", "@tauri-apps/api": "2.9.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2.4.2", @@ -57,32 +57,32 @@ "axios": "^1.13.2", "dayjs": "1.11.19", "foxact": "^0.2.49", - "i18next": "^25.7.1", + "i18next": "^25.7.3", "js-yaml": "^4.1.1", - "lodash-es": "^4.17.21", + "lodash-es": "^4.17.22", "monaco-editor": "^0.55.1", "monaco-yaml": "^5.4.0", "nanoid": "^5.1.6", - "react": "19.2.0", - "react-dom": "19.2.0", + "react": "19.2.3", + "react-dom": "19.2.3", "react-error-boundary": "6.0.0", - "react-hook-form": "^7.67.0", - "react-i18next": "16.3.5", + "react-hook-form": "^7.68.0", + "react-i18next": "16.5.0", "react-markdown": "10.1.0", - "react-router": "^7.10.0", - "react-virtuoso": "^4.16.1", - "swr": "^2.3.7", - "tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main", + "react-router": "^7.11.0", + "react-virtuoso": "^4.17.0", + "swr": "^2.3.8", + "tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main", "types-pac": "^1.0.3" }, "devDependencies": { "@actions/github": "^6.0.1", - "@eslint-react/eslint-plugin": "^2.3.11", - "@eslint/js": "^9.39.1", - "@tauri-apps/cli": "2.9.5", + "@eslint-react/eslint-plugin": "^2.3.13", + "@eslint/js": "^9.39.2", + "@tauri-apps/cli": "2.9.6", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.10.1", + "@types/node": "^24.10.4", "@types/react": "19.2.7", "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "^7.2.1", @@ -91,13 +91,13 @@ "cli-color": "^2.0.4", "commander": "^14.0.2", "cross-env": "^10.1.0", - "eslint": "^9.39.1", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-unused-imports": "^4.3.0", "glob": "^13.0.0", "globals": "^16.5.0", @@ -106,13 +106,13 @@ "jiti": "^2.6.1", "lint-staged": "^16.2.7", "node-fetch": "^3.3.2", - "prettier": "^3.7.3", - "sass": "^1.94.2", + "prettier": "^3.7.4", + "sass": "^1.97.0", "tar": "^7.5.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.2.6", + "typescript-eslint": "^8.50.0", + "vite": "^7.3.0", "vite-plugin-svgr": "^4.5.0" }, "lint-staged": { @@ -125,5 +125,15 @@ ] }, "type": "module", - "packageManager": "pnpm@10.22.0" + "packageManager": "pnpm@10.26.1", + "pnpm": { + "onlyBuiltDependencies": [ + "@parcel/watcher", + "@swc/core", + "core-js", + "es5-ext", + "esbuild", + "unrs-resolver" + ] + } } diff --git a/clash-verge-rev/pnpm-lock.yaml b/clash-verge-rev/pnpm-lock.yaml index 9dde93ccd8..604d7897c3 100644 --- a/clash-verge-rev/pnpm-lock.yaml +++ b/clash-verge-rev/pnpm-lock.yaml @@ -10,40 +10,40 @@ importers: dependencies: '@dnd-kit/core': specifier: ^6.3.1 - version: 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@dnd-kit/sortable': specifier: ^10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@19.2.0) + version: 3.2.2(react@19.2.3) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.7)(react@19.2.0) + version: 11.14.0(@types/react@19.2.7)(react@19.2.3) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 '@monaco-editor/react': specifier: ^4.7.0 - version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@mui/icons-material': - specifier: ^7.3.5 - version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + specifier: ^7.3.6 + version: 7.3.6(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@mui/material': - specifier: ^7.3.5 - version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^7.3.6 + version: 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-table': specifier: ^8.21.3 - version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-virtual': - specifier: ^3.13.12 - version: 3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^3.13.13 + version: 3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tauri-apps/api': specifier: 2.9.1 version: 2.9.1 @@ -70,7 +70,7 @@ importers: version: 2.9.0 ahooks: specifier: ^3.9.6 - version: 3.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.9.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) axios: specifier: ^1.13.2 version: 1.13.2 @@ -79,16 +79,16 @@ importers: version: 1.11.19 foxact: specifier: ^0.2.49 - version: 0.2.49(react@19.2.0) + version: 0.2.49(react@19.2.3) i18next: - specifier: ^25.7.1 - version: 25.7.1(typescript@5.9.3) + specifier: ^25.7.3 + version: 25.7.3(typescript@5.9.3) js-yaml: specifier: ^4.1.1 version: 4.1.1 lodash-es: - specifier: ^4.17.21 - version: 4.17.21 + specifier: ^4.17.22 + version: 4.17.22 monaco-editor: specifier: ^0.55.1 version: 0.55.1 @@ -99,35 +99,35 @@ importers: specifier: ^5.1.6 version: 5.1.6 react: - specifier: 19.2.0 - version: 19.2.0 + specifier: 19.2.3 + version: 19.2.3 react-dom: - specifier: 19.2.0 - version: 19.2.0(react@19.2.0) + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) react-error-boundary: specifier: 6.0.0 - version: 6.0.0(react@19.2.0) + version: 6.0.0(react@19.2.3) react-hook-form: - specifier: ^7.67.0 - version: 7.67.0(react@19.2.0) + specifier: ^7.68.0 + version: 7.68.0(react@19.2.3) react-i18next: - specifier: 16.3.5 - version: 16.3.5(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.5.0 + version: 16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.2.7)(react@19.2.0) + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) react-router: - specifier: ^7.10.0 - version: 7.10.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^7.11.0 + version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-virtuoso: - specifier: ^4.16.1 - version: 4.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^4.17.0 + version: 4.17.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) swr: - specifier: ^2.3.7 - version: 2.3.7(react@19.2.0) + specifier: ^2.3.8 + version: 2.3.8(react@19.2.3) tauri-plugin-mihomo-api: - specifier: git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main - version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210 + specifier: github:clash-verge-rev/tauri-plugin-mihomo#main + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288 types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -136,14 +136,14 @@ importers: specifier: ^6.0.1 version: 6.0.1 '@eslint-react/eslint-plugin': - specifier: ^2.3.11 - version: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^2.3.13 + version: 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@eslint/js': - specifier: ^9.39.1 - version: 9.39.1 + specifier: ^9.39.2 + version: 9.39.2 '@tauri-apps/cli': - specifier: 2.9.5 - version: 2.9.5 + specifier: 2.9.6 + version: 2.9.6 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -151,8 +151,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.10.1 - version: 24.10.1 + specifier: ^24.10.4 + version: 24.10.4 '@types/react': specifier: 19.2.7 version: 19.2.7 @@ -161,10 +161,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-legacy': specifier: ^7.2.1 - version: 7.2.1(terser@5.44.1)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)) + version: 7.2.1(terser@5.44.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2)) '@vitejs/plugin-react-swc': specifier: ^4.2.2 - version: 4.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)) + version: 4.2.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -178,29 +178,29 @@ importers: specifier: ^10.1.0 version: 10.1.0 eslint: - specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + specifier: ^9.39.2 + version: 9.39.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.3) + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-refresh: - specifier: ^0.4.24 - version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + specifier: ^0.4.26 + version: 0.4.26(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) glob: specifier: ^13.0.0 version: 13.0.0 @@ -223,11 +223,11 @@ importers: specifier: ^3.3.2 version: 3.3.2 prettier: - specifier: ^3.7.3 - version: 3.7.3 + specifier: ^3.7.4 + version: 3.7.4 sass: - specifier: ^1.94.2 - version: 1.94.2 + specifier: ^1.97.0 + version: 1.97.0 tar: specifier: ^7.5.2 version: 7.5.2 @@ -238,14 +238,14 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.48.1 - version: 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.50.0 + version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: - specifier: ^7.2.6 - version: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) + specifier: ^7.3.0 + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)) + version: 4.5.0(rollup@4.53.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2)) packages: @@ -259,16 +259,16 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.0': - resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -279,14 +279,14 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.27.1': - resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.27.1': - resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -300,8 +300,8 @@ packages: resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.27.1': - resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.27.1': @@ -342,29 +342,29 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.27.1': - resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + '@babel/helper-wrap-function@7.28.3': + resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==} engines: {node: '>=6.9.0'} '@babel/helpers@7.28.4': resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': - resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -387,8 +387,8 @@ packages: peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': - resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': + resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -441,8 +441,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.28.0': - resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==} + '@babel/plugin-transform-block-scoping@7.28.5': + resolution: {integrity: sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -453,14 +453,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.27.1': - resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.0': - resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==} + '@babel/plugin-transform-classes@7.28.4': + resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -471,8 +471,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.28.0': - resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -507,8 +507,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.27.1': - resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + '@babel/plugin-transform-exponentiation-operator@7.28.5': + resolution: {integrity: sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -543,8 +543,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.27.1': - resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + '@babel/plugin-transform-logical-assignment-operators@7.28.5': + resolution: {integrity: sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -567,8 +567,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.27.1': - resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + '@babel/plugin-transform-modules-systemjs@7.28.5': + resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -603,8 +603,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.28.0': - resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==} + '@babel/plugin-transform-object-rest-spread@7.28.4': + resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -621,8 +621,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.27.1': - resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + '@babel/plugin-transform-optional-chaining@7.28.5': + resolution: {integrity: sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -651,8 +651,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.28.1': - resolution: {integrity: sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==} + '@babel/plugin-transform-regenerator@7.28.4': + resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -723,8 +723,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.28.0': - resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==} + '@babel/preset-env@7.28.5': + resolution: {integrity: sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -742,12 +742,12 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} '@dnd-kit/accessibility@3.1.1': @@ -772,11 +772,11 @@ packages: peerDependencies: react: '>=16.8.0' - '@emnapi/core@1.5.0': - resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -790,8 +790,8 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@emotion/is-prop-valid@1.3.1': - resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} @@ -838,200 +838,206 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@esbuild/aix-ppc64@0.25.4': - resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.4': - resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.4': - resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.4': - resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.4': - resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.4': - resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.4': - resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.4': - resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.4': - resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.4': - resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.4': - resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.4': - resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.4': - resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.4': - resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.4': - resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.4': - resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.4': - resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.4': - resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.4': - resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.4': - resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.4': - resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.4': - resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.4': - resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.4': - resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.4': - resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.8.0': - resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@2.3.11': - resolution: {integrity: sha512-6DNGjdAf7npXrcJdZk8yzO6dCn5c1jwPCvH98GWBzVOANtlgtF/jaSD7KR+u8gdCfZEs3hwfI7SyWEwOIBWvFA==} + '@eslint-react/ast@2.3.13': + resolution: {integrity: sha512-OP2rOhHYLx2nfd9uA9uACKZJN9z9rX9uuAMx4PjT75JNOdYr1GgqWQZcYCepyJ+gmVNCyiXcLXuyhavqxCSM8Q==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/core@2.3.11': - resolution: {integrity: sha512-d817I6VmUmH+p+7orrHVfTiFHLcqvwbMQVaMYRN3tAXJukJRqj6vqktfFYdrRm/9MBAxyRy90xuHdHRHwJ1+tg==} + '@eslint-react/core@2.3.13': + resolution: {integrity: sha512-4bWBE+1kApuxJKIrLJH2FuFtCbM4fXfDs6Ou8MNamGoX6hdynlntssvaMZTd/lk/L8dt01H/3btr7xBX4+4BNA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/eff@2.3.11': - resolution: {integrity: sha512-nMyqC4aMdPCU2BcHYhPNG43hxRE6ixkEmq7CLFihvqN7R1P94EzS9osYJVYvEvuaae+23D3MonAuTOa4ktVTYA==} + '@eslint-react/eff@2.3.13': + resolution: {integrity: sha512-byXsssozwh3VaiqcOonAKQgLXgpMVNSxBWFjdfbNhW7+NttorSt950qtiw+P7A9JoRab1OuGYk4MDY5UVBno8Q==} engines: {node: '>=20.19.0'} - '@eslint-react/eslint-plugin@2.3.11': - resolution: {integrity: sha512-9zn4+ltrRp0kevI13y2q8ITvnZ4et+CjAyUNJDYsI/eIwWiBEIl6nkmaV67WjtvAlKUbTBQf5uRI/S5M3qNJtg==} + '@eslint-react/eslint-plugin@2.3.13': + resolution: {integrity: sha512-gq0Z0wADAXvJS8Y/Wk3isK7WIEcfrQGGGdWvorAv0T7MxPd3d32TVwdc1Gx3hVLka3fYq1BBlQ5Fr8e1VgNuIg==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/shared@2.3.11': - resolution: {integrity: sha512-yvxKELAb0W4N8OeUTSEyY1+Po4kuFwepv0gZeAOX5B2w79nmylEdHGXcIabqr6LEELxRSYGXIHSZ8WbUUJWTnw==} + '@eslint-react/shared@2.3.13': + resolution: {integrity: sha512-ESE7dVeOXtem3K6BD6k2wJaFt35kPtTT9SWCL99LFk7pym4OEGoMxPcyB2R7PMWiVudwl63BmiOgQOdaFYPONg==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/var@2.3.11': - resolution: {integrity: sha512-vu1Fyruq+qTY2K/tOPPBacr2f/2Eqm0lJBLHMUijGNWZ43YFq63jDSHwAyPUjnadgatVrhVbup2KPfaDxFfYiQ==} + '@eslint-react/var@2.3.13': + resolution: {integrity: sha512-BozBfUZkzzobD6x/M8XERAnZQ3UvZPsD49zTGFKKU9M/bgsM78HwzxAPLkiu88W55v3sO/Kqf8fQTXT4VEeZ/g==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1049,12 +1055,12 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.1': - resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1073,18 +1079,14 @@ packages: resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -1101,8 +1103,8 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.12': - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -1111,20 +1113,20 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@juggle/resize-observer@3.4.0': resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - '@monaco-editor/loader@1.6.1': - resolution: {integrity: sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==} + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} '@monaco-editor/react@4.7.0': resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} @@ -1133,14 +1135,14 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@mui/core-downloads-tracker@7.3.5': - resolution: {integrity: sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==} + '@mui/core-downloads-tracker@7.3.6': + resolution: {integrity: sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==} - '@mui/icons-material@7.3.5': - resolution: {integrity: sha512-LciL1GLMZ+VlzyHAALSVAR22t8IST4LCXmljcUSx2NOutgO2XnxdIp8ilFbeNf9wpo0iUFbAuoQcB7h+HHIf3A==} + '@mui/icons-material@7.3.6': + resolution: {integrity: sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^7.3.5 + '@mui/material': ^7.3.6 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -1168,13 +1170,13 @@ packages: '@types/react': optional: true - '@mui/material@7.3.5': - resolution: {integrity: sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==} + '@mui/material@7.3.6': + resolution: {integrity: sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material-pigment-css': ^7.3.5 + '@mui/material-pigment-css': ^7.3.6 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1188,8 +1190,8 @@ packages: '@types/react': optional: true - '@mui/private-theming@7.3.5': - resolution: {integrity: sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==} + '@mui/private-theming@7.3.6': + resolution: {integrity: sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1198,8 +1200,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@7.3.5': - resolution: {integrity: sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==} + '@mui/styled-engine@7.3.6': + resolution: {integrity: sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1211,8 +1213,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@7.3.5': - resolution: {integrity: sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==} + '@mui/system@7.3.6': + resolution: {integrity: sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1227,16 +1229,16 @@ packages: '@types/react': optional: true - '@mui/types@7.4.8': - resolution: {integrity: sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==} + '@mui/types@7.4.9': + resolution: {integrity: sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/utils@7.3.5': - resolution: {integrity: sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==} + '@mui/utils@7.3.6': + resolution: {integrity: sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1252,8 +1254,8 @@ packages: resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} - '@octokit/core@5.2.1': - resolution: {integrity: sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==} + '@octokit/core@5.2.2': + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} engines: {node: '>= 18'} '@octokit/endpoint@9.0.6': @@ -1388,8 +1390,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rollup/pluginutils@5.2.0': - resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -1397,103 +1399,113 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.46.2': - resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + '@rollup/rollup-android-arm-eabi@4.53.5': + resolution: {integrity: sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.2': - resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + '@rollup/rollup-android-arm64@4.53.5': + resolution: {integrity: sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.2': - resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + '@rollup/rollup-darwin-arm64@4.53.5': + resolution: {integrity: sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.2': - resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + '@rollup/rollup-darwin-x64@4.53.5': + resolution: {integrity: sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.2': - resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + '@rollup/rollup-freebsd-arm64@4.53.5': + resolution: {integrity: sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.2': - resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + '@rollup/rollup-freebsd-x64@4.53.5': + resolution: {integrity: sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + '@rollup/rollup-linux-arm-gnueabihf@4.53.5': + resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + '@rollup/rollup-linux-arm-musleabihf@4.53.5': + resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.46.2': - resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + '@rollup/rollup-linux-arm64-gnu@4.53.5': + resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.46.2': - resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + '@rollup/rollup-linux-arm64-musl@4.53.5': + resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + '@rollup/rollup-linux-loong64-gnu@4.53.5': + resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + '@rollup/rollup-linux-ppc64-gnu@4.53.5': + resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + '@rollup/rollup-linux-riscv64-gnu@4.53.5': + resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.46.2': - resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + '@rollup/rollup-linux-riscv64-musl@4.53.5': + resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.46.2': - resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + '@rollup/rollup-linux-s390x-gnu@4.53.5': + resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.46.2': - resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + '@rollup/rollup-linux-x64-gnu@4.53.5': + resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.46.2': - resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + '@rollup/rollup-linux-x64-musl@4.53.5': + resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.46.2': - resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + '@rollup/rollup-openharmony-arm64@4.53.5': + resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.5': + resolution: {integrity: sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.2': - resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + '@rollup/rollup-win32-ia32-msvc@4.53.5': + resolution: {integrity: sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.2': - resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + '@rollup/rollup-win32-x64-gnu@4.53.5': + resolution: {integrity: sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.5': + resolution: {integrity: sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==} cpu: [x64] os: [win32] @@ -1568,68 +1580,68 @@ packages: peerDependencies: '@svgr/core': '*' - '@swc/core-darwin-arm64@1.14.0': - resolution: {integrity: sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==} + '@swc/core-darwin-arm64@1.15.7': + resolution: {integrity: sha512-+hNVUfezUid7LeSHqnhoC6Gh3BROABxjlDNInuZ/fie1RUxaEX4qzDwdTgozJELgHhvYxyPIg1ro8ibnKtgO4g==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.14.0': - resolution: {integrity: sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==} + '@swc/core-darwin-x64@1.15.7': + resolution: {integrity: sha512-ZAFuvtSYZTuXPcrhanaD5eyp27H8LlDzx2NAeVyH0FchYcuXf0h5/k3GL9ZU6Jw9eQ63R1E8KBgpXEJlgRwZUQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.14.0': - resolution: {integrity: sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==} + '@swc/core-linux-arm-gnueabihf@1.15.7': + resolution: {integrity: sha512-K3HTYocpqnOw8KcD8SBFxiDHjIma7G/X+bLdfWqf+qzETNBrzOub/IEkq9UaeupaJiZJkPptr/2EhEXXWryS/A==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.14.0': - resolution: {integrity: sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==} + '@swc/core-linux-arm64-gnu@1.15.7': + resolution: {integrity: sha512-HCnVIlsLnCtQ3uXcXgWrvQ6SAraskLA9QJo9ykTnqTH6TvUYqEta+TdTdGjzngD6TOE7XjlAiUs/RBtU8Z0t+Q==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.14.0': - resolution: {integrity: sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==} + '@swc/core-linux-arm64-musl@1.15.7': + resolution: {integrity: sha512-/OOp9UZBg4v2q9+x/U21Jtld0Wb8ghzBScwhscI7YvoSh4E8RALaJ1msV8V8AKkBkZH7FUAFB7Vbv0oVzZsezA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.14.0': - resolution: {integrity: sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==} + '@swc/core-linux-x64-gnu@1.15.7': + resolution: {integrity: sha512-VBbs4gtD4XQxrHuQ2/2+TDZpPQQgrOHYRnS6SyJW+dw0Nj/OomRqH+n5Z4e/TgKRRbieufipeIGvADYC/90PYQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.14.0': - resolution: {integrity: sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==} + '@swc/core-linux-x64-musl@1.15.7': + resolution: {integrity: sha512-kVuy2unodso6p0rMauS2zby8/bhzoGRYxBDyD6i2tls/fEYAE74oP0VPFzxIyHaIjK1SN6u5TgvV9MpyJ5xVug==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.14.0': - resolution: {integrity: sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==} + '@swc/core-win32-arm64-msvc@1.15.7': + resolution: {integrity: sha512-uddYoo5Xmo1XKLhAnh4NBIyy5d0xk33x1sX3nIJboFySLNz878ksCFCZ3IBqrt1Za0gaoIWoOSSSk0eNhAc/sw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.14.0': - resolution: {integrity: sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==} + '@swc/core-win32-ia32-msvc@1.15.7': + resolution: {integrity: sha512-rqq8JjNMLx3QNlh0aPTtN/4+BGLEHC94rj9mkH1stoNRf3ra6IksNHMHy+V1HUqElEgcZyx+0yeXx3eLOTcoFw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.14.0': - resolution: {integrity: sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==} + '@swc/core-win32-x64-msvc@1.15.7': + resolution: {integrity: sha512-4BK06EGdPnuplgcNhmSbOIiLdRgHYX3v1nl4HXo5uo4GZMfllXaCyBUes+0ePRfwbn9OFgVhCWPcYYjMT6hycQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.14.0': - resolution: {integrity: sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==} + '@swc/core@1.15.7': + resolution: {integrity: sha512-kTGB8XI7P+pTKW83tnUEDVP4zduF951u3UAOn5eTi0vyW6MvL56A3+ggMdfuVFtDI0/DsbSzf5z34HVBbuScWw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1650,8 +1662,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.12': - resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + '@tanstack/react-virtual@3.13.13': + resolution: {integrity: sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1660,80 +1672,80 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tanstack/virtual-core@3.13.13': + resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==} '@tauri-apps/api@2.9.1': resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} - '@tauri-apps/cli-darwin-arm64@2.9.5': - resolution: {integrity: sha512-P5XDyCwq3VbWGAplyfP/bgmuUITVDcypxgZUyX45SM7HbU1Nrkk0cNK1HCOkuNBAVVbWen2GUNWah/AiupHHXg==} + '@tauri-apps/cli-darwin-arm64@2.9.6': + resolution: {integrity: sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.9.5': - resolution: {integrity: sha512-JC9UfQ2ZKavx60dnNxsWztRF3oUH3dgPwN1WJ3/5RUy2aNwD/vXqvJAfNFZ4GWeQpoQ+PqJxduev0U4OMQonnA==} + '@tauri-apps/cli-darwin-x64@2.9.6': + resolution: {integrity: sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.5': - resolution: {integrity: sha512-iCQm2Uvx8AheghfG/QUv1y8Ga9yquJt6xJwH1uF0x5KfmJmwBi8pHBvB924dDi59PS84qTdIBeJejQT00QX3Iw==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': + resolution: {integrity: sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.9.5': - resolution: {integrity: sha512-b6AW8Gr5nQOQIYH0TsUev7rEThGHIvsx192eElOmOz/dh33J4pninHK32laMj2hzHMJ27qmDq5vANL+wrFo9sg==} + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': + resolution: {integrity: sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.9.5': - resolution: {integrity: sha512-/gRBMnphS9E8riZ0LIbBhZ9Oy16A2rx/g3DGR0DcDBvUtkLfbL0lMu4s+sY85nkn9An15+cZ1ZK6d7AIqWahLA==} + '@tauri-apps/cli-linux-arm64-musl@2.9.6': + resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.9.5': - resolution: {integrity: sha512-NOzjPF9YIBodjdkFcJmqINT0k3YDoR5ANM/jg6Z6s3Zmk8ScN6inI60jTxcfgfWyITiKsPy7GJyYou3Cm2XNzw==} + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': + resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.9.5': - resolution: {integrity: sha512-SfGbwgvTphM5y+J91NyU/psleMUlyyPkZyDCFg8WU1HX8DpKUT3Vwhb/W1xpUBGb56tJgGCO46FCVkr8w4Areg==} + '@tauri-apps/cli-linux-x64-gnu@2.9.6': + resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.9.5': - resolution: {integrity: sha512-ZfeoiASAOGDzyvN+TDAg8A1pCeS082h4uc0vZKvtWUN+9QBIMfz0yJwltAv+SN/afap6NS6DVkbPV3UVuI9V5A==} + '@tauri-apps/cli-linux-x64-musl@2.9.6': + resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.9.5': - resolution: {integrity: sha512-ulg7irow+ekjaK4inFHVq7m1KQebDSYNb17DFKV+h+x7qnLZymz2gHK7df2u4YyEjqvzwRd3AJpU3HNxRurSFQ==} + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': + resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.9.5': - resolution: {integrity: sha512-6lF0k/Qduhn1Z3IOXlp2ts8jNOMIX4cK4Fbk3axGeX7LMcVVbOSEAFwbTqS8BKZDFac0WRS8N1C96+Ms5LOS1Q==} + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': + resolution: {integrity: sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.9.5': - resolution: {integrity: sha512-Vg50U74x1A4b2iBVtDcAVPbI1XVuzSmwlduuBM1VewxtRaVj5GDzWnYtBcnuIk+VGzNApRDfDhraAXGaW2a/Gw==} + '@tauri-apps/cli-win32-x64-msvc@2.9.6': + resolution: {integrity: sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.9.5': - resolution: {integrity: sha512-z88tX6O6kwTgMjYozhNGbehzQyBazgXejyH784CwSfBOWm06xFcogd0PY/jhcPsqzJF9kLRIkmlQy+cqdrioOQ==} + '@tauri-apps/cli@2.9.6': + resolution: {integrity: sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==} engines: {node: '>= 10'} hasBin: true @@ -1788,8 +1800,8 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.16': - resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1797,8 +1809,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1828,63 +1840,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.48.1': - resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} + '@typescript-eslint/eslint-plugin@8.50.0': + resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.48.1 + '@typescript-eslint/parser': ^8.50.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.48.1': - resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} + '@typescript-eslint/parser@8.50.0': + resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.48.1': - resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} + '@typescript-eslint/project-service@8.50.0': + resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.48.1': - resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} + '@typescript-eslint/scope-manager@8.50.0': + resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.48.1': - resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} + '@typescript-eslint/tsconfig-utils@8.50.0': + resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.48.1': - resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} + '@typescript-eslint/type-utils@8.50.0': + resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.48.1': - resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} + '@typescript-eslint/types@8.50.0': + resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.48.1': - resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} + '@typescript-eslint/typescript-estree@8.50.0': + resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.48.1': - resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} + '@typescript-eslint/utils@8.50.0': + resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.48.1': - resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} + '@typescript-eslint/visitor-keys@8.50.0': + resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -2012,8 +2024,8 @@ packages: resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} engines: {node: '>=12.0'} - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} ahooks@3.9.6: @@ -2025,20 +2037,20 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ansi-escapes@7.1.1: - resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} + ansi-escapes@7.2.0: + resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} argparse@2.0.1: @@ -2107,6 +2119,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.9.10: + resolution: {integrity: sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==} + hasBin: true + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -2130,8 +2146,8 @@ packages: peerDependencies: browserslist: '*' - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2158,8 +2174,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001760: + resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2196,8 +2212,8 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-truncate@5.1.0: - resolution: {integrity: sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==} + cli-truncate@5.1.1: + resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} client-only@0.0.1: @@ -2247,15 +2263,15 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - core-js-compat@3.44.0: - resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} + core-js-compat@3.47.0: + resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} - core-js@3.45.0: - resolution: {integrity: sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} @@ -2322,8 +2338,8 @@ packages: supports-color: optional: true - decode-named-character-reference@1.1.0: - resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2372,11 +2388,11 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.173: - resolution: {integrity: sha512-2bFhXP2zqSfQHugjqJIDFVwa+qIxyNApenmXTp9EjaKtdPrES5Qcn9/aSFy/NaP2E+fWG/zxKu/LBvY36p5VNQ==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - emoji-regex@10.5.0: - resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} @@ -2386,11 +2402,11 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -2431,8 +2447,8 @@ packages: es6-weak-map@2.0.3: resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} - esbuild@0.25.4: - resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} hasBin: true @@ -2533,15 +2549,15 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-dom@2.3.11: - resolution: {integrity: sha512-++1BN87Nn7smgD1iZiQXRUE3ypcMXo7FeD+hTcL4d3aa32ur6vF30xkPmSfBs+mPCuGygNnRN8uvSWtkkWCa5w==} + eslint-plugin-react-dom@2.3.13: + resolution: {integrity: sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-hooks-extra@2.3.11: - resolution: {integrity: sha512-bBxCWIrt0cKKCIa8wj36EYnt1Hw3Z93yCPph3FEmzQTClDTJkCoQQKhTTuofMHvAVRSeYG0TJnCtNW34XwQs0Q==} + eslint-plugin-react-hooks-extra@2.3.13: + resolution: {integrity: sha512-NSnY8yvtrvu2FAALLuvc2xesIAkMqGyJgilpy8wEi1w/Nw6v0IwBEffoNKLq9OHW4v3nikud3aBTqWfWKOx67Q==} engines: {node: '>=20.0.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2553,27 +2569,27 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@2.3.11: - resolution: {integrity: sha512-fN/EjnmMEbv6EADSyNZcs3KjPdWEI5A6i1rQqDk93KQhITXWNMbXHy/bn2a4Q3ZIfgWtJbwVE7RhcSOqJGFwVA==} + eslint-plugin-react-naming-convention@2.3.13: + resolution: {integrity: sha512-2iler1ldFpB/PaNpN8WAVk6dKYKwKcoGm1j0JAAjdCrsfOTJ007ol2xTAyoHKAbMOvkZSi7qq90q+Q//RuhWwA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-refresh@0.4.24: - resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} peerDependencies: eslint: '>=8.40' - eslint-plugin-react-web-api@2.3.11: - resolution: {integrity: sha512-YU1WMWOpJNKVi23Mn+4C508u1ueo+eggAZToAAGIe31BCRiJpAw3YpOJEIPmkUMYhqNU46zB5KhhpAKwb8oj1w==} + eslint-plugin-react-web-api@2.3.13: + resolution: {integrity: sha512-+UypRPHP9GFMulIENpsC/J+TygWywiyz2mb4qyUP6y/IwdcSilk1MyF9WquNYKB/4/FN4Rl1oRm6WMbfkbpMnQ==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-x@2.3.11: - resolution: {integrity: sha512-MJ3kFsrP1CVICXhzlBPVLnO/2f/FXZMIwRMsCDMwm+liIzZ+WZqvdOBJbb/F7UBKVD2LIvrr51wWJIVJpBFbtg==} + eslint-plugin-react-x@2.3.13: + resolution: {integrity: sha512-+m+V/5VLMxgx0VsFUUyflMNLQG0WFYspsfv0XJFqx7me3A2b3P20QatNDHQCYswz0PRbRFqinTPukPRhZh68ag==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2600,8 +2616,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.1: - resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2699,8 +2715,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2712,8 +2728,8 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} formdata-polyfill@4.0.10: @@ -2743,6 +2759,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2763,8 +2783,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.10.1: - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -2790,9 +2810,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2850,8 +2867,8 @@ packages: engines: {node: '>=18'} hasBin: true - i18next@25.7.1: - resolution: {integrity: sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw==} + i18next@25.7.3: + resolution: {integrity: sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -2866,8 +2883,8 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - immutable@5.1.2: - resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==} + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -2877,8 +2894,8 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} @@ -2947,8 +2964,8 @@ packages: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-glob@4.0.3: @@ -3044,11 +3061,6 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3101,8 +3113,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash-es@4.17.22: + resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -3127,8 +3139,8 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lru-cache@11.1.0: - resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3164,8 +3176,8 @@ packages: mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -3321,8 +3333,8 @@ packages: engines: {node: ^18 || >=20} hasBin: true - napi-postinstall@0.3.3: - resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -3347,8 +3359,8 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -3426,8 +3438,8 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} path-type@4.0.0: @@ -3466,8 +3478,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.7.3: - resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true @@ -3484,10 +3496,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: - react: ^19.2.0 + react: ^19.2.3 react-error-boundary@6.0.0: resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} @@ -3497,14 +3509,14 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.67.0: - resolution: {integrity: sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==} + react-hook-form@7.68.0: + resolution: {integrity: sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.3.5: - resolution: {integrity: sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==} + react-i18next@16.5.0: + resolution: {integrity: sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==} peerDependencies: i18next: '>= 25.6.2' react: '>= 16.8.0' @@ -3522,8 +3534,8 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@19.2.0: - resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + react-is@19.2.3: + resolution: {integrity: sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==} react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} @@ -3531,8 +3543,8 @@ packages: '@types/react': '>=18' react: '>=18' - react-router@7.10.0: - resolution: {integrity: sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw==} + react-router@7.11.0: + resolution: {integrity: sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3547,14 +3559,14 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-virtuoso@4.16.1: - resolution: {integrity: sha512-V9ZDw7TFspJb02gNWqHyVZvaMaCFaoL30F/tOVepCI12kdLjA2oxFfWvNC66AVJdOH5cwiq8317p2Q9OpG+TDw==} + react-virtuoso@4.17.0: + resolution: {integrity: sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w==} peerDependencies: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} readdirp@4.1.2: @@ -3565,8 +3577,8 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerate-unicode-properties@10.2.0: - resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} regenerate@1.4.2: @@ -3579,15 +3591,15 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - regexpu-core@6.2.0: - resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} regjsgen@0.8.0: resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - regjsparser@0.12.0: - resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true remark-parse@11.0.0: @@ -3606,8 +3618,8 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true @@ -3618,8 +3630,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.46.2: - resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + rollup@4.53.5: + resolution: {integrity: sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3635,8 +3647,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass@1.94.2: - resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==} + sass@1.97.0: + resolution: {integrity: sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -3651,16 +3663,16 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -3768,8 +3780,8 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} strip-bom@3.0.0: @@ -3780,11 +3792,11 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - style-to-js@1.1.16: - resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} - style-to-object@1.0.8: - resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -3800,8 +3812,8 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - swr@2.3.7: - resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==} + swr@2.3.8: + resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3816,8 +3828,8 @@ packages: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210: - resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210} + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288: + resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288} version: 0.1.0 terser@5.44.1: @@ -3893,8 +3905,8 @@ packages: types-pac@1.0.3: resolution: {integrity: sha512-MF2UAZGvGMOM+vHi9Zj/LvQqdNN1m1xSB+PjAW9B/GvFqaB4GwR18YaIbGIGDRTW/J8iqFXQHLZd5eJVtho46w==} - typescript-eslint@8.48.1: - resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==} + typescript-eslint@8.50.0: + resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3924,19 +3936,19 @@ packages: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} - unicode-match-property-value-ecmascript@2.2.0: - resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} engines: {node: '>=4'} - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} @@ -3944,8 +3956,8 @@ packages: unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} @@ -3956,8 +3968,8 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -3970,8 +3982,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -3981,8 +3993,8 @@ packages: peerDependencies: vite: '>=2.6.0' - vite@7.2.6: - resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4088,8 +4100,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -4103,8 +4115,8 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.1.13: - resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4114,9 +4126,9 @@ snapshots: '@actions/github@6.0.1': dependencies: '@actions/http-client': 2.2.3 - '@octokit/core': 5.2.1 - '@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.1) - '@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.1) + '@octokit/core': 5.2.2 + '@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.2) + '@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.2) '@octokit/request': 8.4.1 '@octokit/request-error': 5.1.1 undici: 5.29.0 @@ -4128,23 +4140,23 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.0': {} + '@babel/compat-data@7.28.5': {} - '@babel/core@7.28.4': + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -4154,605 +4166,605 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.0 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.1 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.4)': + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.4)': + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.2.0 + regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.4)': + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 debug: 4.4.3 lodash.debounce: 4.0.8 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.27.1': + '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.4)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/core': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} - '@babel/helper-wrap-function@7.27.1': + '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-logical-assignment-operators@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.4)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.4)': + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-env@7.28.0(@babel/core@7.28.4)': + '@babel/preset-env@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.4 + '@babel/compat-data': 7.28.5 + '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.4) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.28.4) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.4) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.4) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.4) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4) - core-js-compat: 3.44.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.28.5) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.5) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.5) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-block-scoping': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.5) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.5) + '@babel/plugin-transform-exponentiation-operator': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-logical-assignment-operators': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.5) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.5) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.5) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.5) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.5) + core-js-compat: 3.47.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.4)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 esutils: 2.0.3 '@babel/runtime@7.28.4': {} @@ -4760,58 +4772,58 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.28.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - '@dnd-kit/accessibility@3.1.1(react@19.2.0)': + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': dependencies: - react: 19.2.0 + react: 19.2.3 tslib: 2.8.1 - '@dnd-kit/core@6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.2.0) - '@dnd-kit/utilities': 3.2.2(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@dnd-kit/accessibility': 3.1.1(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@dnd-kit/utilities': 3.2.2(react@19.2.0) - react: 19.2.0 + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 tslib: 2.8.1 - '@dnd-kit/utilities@3.2.2(react@19.2.0)': + '@dnd-kit/utilities@3.2.2(react@19.2.3)': dependencies: - react: 19.2.0 + react: 19.2.3 tslib: 2.8.1 - '@emnapi/core@1.5.0': + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.5.0': + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true @@ -4847,23 +4859,23 @@ snapshots: '@emotion/hash@0.9.2': {} - '@emotion/is-prop-valid@1.3.1': + '@emotion/is-prop-valid@1.4.0': dependencies: '@emotion/memoize': 0.9.0 '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0)': + '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.3) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.2.0 + react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 transitivePeerDependencies: @@ -4879,16 +4891,16 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 - '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.3) '@emotion/utils': 1.4.2 - react: 19.2.0 + react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 transitivePeerDependencies: @@ -4896,9 +4908,9 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.0)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.3)': dependencies: - react: 19.2.0 + react: 19.2.3 '@emotion/utils@1.4.2': {} @@ -4906,156 +4918,159 @@ snapshots: '@epic-web/invariant@1.0.0': {} - '@esbuild/aix-ppc64@0.25.4': + '@esbuild/aix-ppc64@0.27.2': optional: true - '@esbuild/android-arm64@0.25.4': + '@esbuild/android-arm64@0.27.2': optional: true - '@esbuild/android-arm@0.25.4': + '@esbuild/android-arm@0.27.2': optional: true - '@esbuild/android-x64@0.25.4': + '@esbuild/android-x64@0.27.2': optional: true - '@esbuild/darwin-arm64@0.25.4': + '@esbuild/darwin-arm64@0.27.2': optional: true - '@esbuild/darwin-x64@0.25.4': + '@esbuild/darwin-x64@0.27.2': optional: true - '@esbuild/freebsd-arm64@0.25.4': + '@esbuild/freebsd-arm64@0.27.2': optional: true - '@esbuild/freebsd-x64@0.25.4': + '@esbuild/freebsd-x64@0.27.2': optional: true - '@esbuild/linux-arm64@0.25.4': + '@esbuild/linux-arm64@0.27.2': optional: true - '@esbuild/linux-arm@0.25.4': + '@esbuild/linux-arm@0.27.2': optional: true - '@esbuild/linux-ia32@0.25.4': + '@esbuild/linux-ia32@0.27.2': optional: true - '@esbuild/linux-loong64@0.25.4': + '@esbuild/linux-loong64@0.27.2': optional: true - '@esbuild/linux-mips64el@0.25.4': + '@esbuild/linux-mips64el@0.27.2': optional: true - '@esbuild/linux-ppc64@0.25.4': + '@esbuild/linux-ppc64@0.27.2': optional: true - '@esbuild/linux-riscv64@0.25.4': + '@esbuild/linux-riscv64@0.27.2': optional: true - '@esbuild/linux-s390x@0.25.4': + '@esbuild/linux-s390x@0.27.2': optional: true - '@esbuild/linux-x64@0.25.4': + '@esbuild/linux-x64@0.27.2': optional: true - '@esbuild/netbsd-arm64@0.25.4': + '@esbuild/netbsd-arm64@0.27.2': optional: true - '@esbuild/netbsd-x64@0.25.4': + '@esbuild/netbsd-x64@0.27.2': optional: true - '@esbuild/openbsd-arm64@0.25.4': + '@esbuild/openbsd-arm64@0.27.2': optional: true - '@esbuild/openbsd-x64@0.25.4': + '@esbuild/openbsd-x64@0.27.2': optional: true - '@esbuild/sunos-x64@0.25.4': + '@esbuild/openharmony-arm64@0.27.2': optional: true - '@esbuild/win32-arm64@0.25.4': + '@esbuild/sunos-x64@0.27.2': optional: true - '@esbuild/win32-ia32@0.25.4': + '@esbuild/win32-arm64@0.27.2': optional: true - '@esbuild/win32-x64@0.25.4': + '@esbuild/win32-ia32@0.27.2': optional: true - '@eslint-community/eslint-utils@4.8.0(eslint@9.39.1(jiti@2.6.1))': + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': dependencies: - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.11 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-react/eff': 2.3.13 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) string-ts: 2.3.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/core@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/core@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) birecord: 0.1.1 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/eff@2.3.11': {} + '@eslint-react/eff@2.3.13': {} - '@eslint-react/eslint-plugin@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-dom: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-plugin-react-dom: 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.11 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-react/eff': 2.3.13 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 - zod: 4.1.13 + zod: 4.2.1 transitivePeerDependencies: - supports-color - '@eslint-react/var@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: @@ -5077,7 +5092,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -5091,7 +5106,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.1': {} + '@eslint/js@9.39.2': {} '@eslint/object-schema@2.1.7': {} @@ -5104,15 +5119,13 @@ snapshots: '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.3': {} '@isaacs/balanced-match@4.0.1': {} @@ -5125,100 +5138,100 @@ snapshots: dependencies: minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.12': + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.29': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 '@juggle/resize-observer@3.4.0': {} - '@monaco-editor/loader@1.6.1': + '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@monaco-editor/loader': 1.6.1 + '@monaco-editor/loader': 1.7.0 monaco-editor: 0.55.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - '@mui/core-downloads-tracker@7.3.5': {} + '@mui/core-downloads-tracker@7.3.6': {} - '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)': + '@mui/icons-material@7.3.6(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 + '@mui/material': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 - '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.7) - '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/material': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@mui/system': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) + '@mui/types': 7.4.9(@types/react@19.2.7) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.3) clsx: 2.1.1 prop-types: 15.8.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) '@types/react': 19.2.7 - '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 - '@mui/core-downloads-tracker': 7.3.5 - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.7) - '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/core-downloads-tracker': 7.3.6 + '@mui/system': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) + '@mui/types': 7.4.9(@types/react@19.2.7) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.3) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.12(@types/react@19.2.7) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-is: 19.2.0 - react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 19.2.3 + react-transition-group: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) '@types/react': 19.2.7 - '@mui/private-theming@7.3.5(@types/react@19.2.7)(react@19.2.0)': + '@mui/private-theming@7.3.6(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.3) prop-types: 15.8.1 - react: 19.2.0 + react: 19.2.3 optionalDependencies: '@types/react': 19.2.7 - '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(react@19.2.0)': + '@mui/styled-engine@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 @@ -5226,55 +5239,55 @@ snapshots: '@emotion/sheet': 1.4.0 csstype: 3.2.3 prop-types: 15.8.1 - react: 19.2.0 + react: 19.2.3 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) - '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)': + '@mui/system@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 - '@mui/private-theming': 7.3.5(@types/react@19.2.7)(react@19.2.0) - '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.7) - '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/private-theming': 7.3.6(@types/react@19.2.7)(react@19.2.3) + '@mui/styled-engine': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(react@19.2.3) + '@mui/types': 7.4.9(@types/react@19.2.7) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.3) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 - react: 19.2.0 + react: 19.2.3 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) '@types/react': 19.2.7 - '@mui/types@7.4.8(@types/react@19.2.7)': + '@mui/types@7.4.9(@types/react@19.2.7)': dependencies: '@babel/runtime': 7.28.4 optionalDependencies: '@types/react': 19.2.7 - '@mui/utils@7.3.5(@types/react@19.2.7)(react@19.2.0)': + '@mui/utils@7.3.6(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.7) + '@mui/types': 7.4.9(@types/react@19.2.7) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 - react: 19.2.0 - react-is: 19.2.0 + react: 19.2.3 + react-is: 19.2.3 optionalDependencies: '@types/react': 19.2.7 '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true '@octokit/auth-token@4.0.0': {} - '@octokit/core@5.2.1': + '@octokit/core@5.2.2': dependencies: '@octokit/auth-token': 4.0.0 '@octokit/graphql': 7.1.1 @@ -5299,14 +5312,14 @@ snapshots: '@octokit/openapi-types@24.2.0': {} - '@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.1)': + '@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 5.2.1 + '@octokit/core': 5.2.2 '@octokit/types': 12.6.0 - '@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.1)': + '@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 5.2.1 + '@octokit/core': 5.2.2 '@octokit/types': 12.6.0 '@octokit/request-error@5.1.1': @@ -5397,125 +5410,131 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rollup/pluginutils@5.2.0(rollup@4.46.2)': + '@rollup/pluginutils@5.3.0(rollup@4.53.5)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.46.2 + rollup: 4.53.5 - '@rollup/rollup-android-arm-eabi@4.46.2': + '@rollup/rollup-android-arm-eabi@4.53.5': optional: true - '@rollup/rollup-android-arm64@4.46.2': + '@rollup/rollup-android-arm64@4.53.5': optional: true - '@rollup/rollup-darwin-arm64@4.46.2': + '@rollup/rollup-darwin-arm64@4.53.5': optional: true - '@rollup/rollup-darwin-x64@4.46.2': + '@rollup/rollup-darwin-x64@4.53.5': optional: true - '@rollup/rollup-freebsd-arm64@4.46.2': + '@rollup/rollup-freebsd-arm64@4.53.5': optional: true - '@rollup/rollup-freebsd-x64@4.46.2': + '@rollup/rollup-freebsd-x64@4.53.5': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + '@rollup/rollup-linux-arm-gnueabihf@4.53.5': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.2': + '@rollup/rollup-linux-arm-musleabihf@4.53.5': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.2': + '@rollup/rollup-linux-arm64-gnu@4.53.5': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.2': + '@rollup/rollup-linux-arm64-musl@4.53.5': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + '@rollup/rollup-linux-loong64-gnu@4.53.5': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.2': + '@rollup/rollup-linux-ppc64-gnu@4.53.5': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.2': + '@rollup/rollup-linux-riscv64-gnu@4.53.5': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.2': + '@rollup/rollup-linux-riscv64-musl@4.53.5': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.2': + '@rollup/rollup-linux-s390x-gnu@4.53.5': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.2': + '@rollup/rollup-linux-x64-gnu@4.53.5': optional: true - '@rollup/rollup-linux-x64-musl@4.46.2': + '@rollup/rollup-linux-x64-musl@4.53.5': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.2': + '@rollup/rollup-openharmony-arm64@4.53.5': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.2': + '@rollup/rollup-win32-arm64-msvc@4.53.5': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.2': + '@rollup/rollup-win32-ia32-msvc@4.53.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.5': optional: true '@rtsao/scc@1.1.0': optional: true - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.4)': + '@svgr/babel-preset@8.1.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.4) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 @@ -5525,64 +5544,64 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@swc/core-darwin-arm64@1.14.0': + '@swc/core-darwin-arm64@1.15.7': optional: true - '@swc/core-darwin-x64@1.14.0': + '@swc/core-darwin-x64@1.15.7': optional: true - '@swc/core-linux-arm-gnueabihf@1.14.0': + '@swc/core-linux-arm-gnueabihf@1.15.7': optional: true - '@swc/core-linux-arm64-gnu@1.14.0': + '@swc/core-linux-arm64-gnu@1.15.7': optional: true - '@swc/core-linux-arm64-musl@1.14.0': + '@swc/core-linux-arm64-musl@1.15.7': optional: true - '@swc/core-linux-x64-gnu@1.14.0': + '@swc/core-linux-x64-gnu@1.15.7': optional: true - '@swc/core-linux-x64-musl@1.14.0': + '@swc/core-linux-x64-musl@1.15.7': optional: true - '@swc/core-win32-arm64-msvc@1.14.0': + '@swc/core-win32-arm64-msvc@1.15.7': optional: true - '@swc/core-win32-ia32-msvc@1.14.0': + '@swc/core-win32-ia32-msvc@1.15.7': optional: true - '@swc/core-win32-x64-msvc@1.14.0': + '@swc/core-win32-x64-msvc@1.15.7': optional: true - '@swc/core@1.14.0': + '@swc/core@1.15.7': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.14.0 - '@swc/core-darwin-x64': 1.14.0 - '@swc/core-linux-arm-gnueabihf': 1.14.0 - '@swc/core-linux-arm64-gnu': 1.14.0 - '@swc/core-linux-arm64-musl': 1.14.0 - '@swc/core-linux-x64-gnu': 1.14.0 - '@swc/core-linux-x64-musl': 1.14.0 - '@swc/core-win32-arm64-msvc': 1.14.0 - '@swc/core-win32-ia32-msvc': 1.14.0 - '@swc/core-win32-x64-msvc': 1.14.0 + '@swc/core-darwin-arm64': 1.15.7 + '@swc/core-darwin-x64': 1.15.7 + '@swc/core-linux-arm-gnueabihf': 1.15.7 + '@swc/core-linux-arm64-gnu': 1.15.7 + '@swc/core-linux-arm64-musl': 1.15.7 + '@swc/core-linux-x64-gnu': 1.15.7 + '@swc/core-linux-x64-musl': 1.15.7 + '@swc/core-win32-arm64-msvc': 1.15.7 + '@swc/core-win32-ia32-msvc': 1.15.7 + '@swc/core-win32-x64-msvc': 1.15.7 '@swc/counter@0.1.3': {} @@ -5590,70 +5609,70 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/table-core': 8.21.3 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - '@tanstack/react-virtual@3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-virtual@3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/virtual-core': 3.13.12 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@tanstack/virtual-core': 3.13.13 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.12': {} + '@tanstack/virtual-core@3.13.13': {} '@tauri-apps/api@2.9.1': {} - '@tauri-apps/cli-darwin-arm64@2.9.5': + '@tauri-apps/cli-darwin-arm64@2.9.6': optional: true - '@tauri-apps/cli-darwin-x64@2.9.5': + '@tauri-apps/cli-darwin-x64@2.9.6': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.5': + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.9.5': + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.9.5': + '@tauri-apps/cli-linux-arm64-musl@2.9.6': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.9.5': + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.9.5': + '@tauri-apps/cli-linux-x64-gnu@2.9.6': optional: true - '@tauri-apps/cli-linux-x64-musl@2.9.5': + '@tauri-apps/cli-linux-x64-musl@2.9.6': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.9.5': + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.9.5': + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.9.5': + '@tauri-apps/cli-win32-x64-msvc@2.9.6': optional: true - '@tauri-apps/cli@2.9.5': + '@tauri-apps/cli@2.9.6': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.9.5 - '@tauri-apps/cli-darwin-x64': 2.9.5 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.5 - '@tauri-apps/cli-linux-arm64-gnu': 2.9.5 - '@tauri-apps/cli-linux-arm64-musl': 2.9.5 - '@tauri-apps/cli-linux-riscv64-gnu': 2.9.5 - '@tauri-apps/cli-linux-x64-gnu': 2.9.5 - '@tauri-apps/cli-linux-x64-musl': 2.9.5 - '@tauri-apps/cli-win32-arm64-msvc': 2.9.5 - '@tauri-apps/cli-win32-ia32-msvc': 2.9.5 - '@tauri-apps/cli-win32-x64-msvc': 2.9.5 + '@tauri-apps/cli-darwin-arm64': 2.9.6 + '@tauri-apps/cli-darwin-x64': 2.9.6 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.6 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.6 + '@tauri-apps/cli-linux-arm64-musl': 2.9.6 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-musl': 2.9.6 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.6 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.6 + '@tauri-apps/cli-win32-x64-msvc': 2.9.6 '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: @@ -5713,9 +5732,9 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.16 + '@types/lodash': 4.17.21 - '@types/lodash@4.17.16': {} + '@types/lodash@4.17.21': {} '@types/mdast@4.0.4': dependencies: @@ -5723,7 +5742,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.10.1': + '@types/node@24.10.4': dependencies: undici-types: 7.16.0 @@ -5750,16 +5769,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.1 - eslint: 9.39.1(jiti@2.6.1) - graphemer: 1.4.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.0 + eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -5767,79 +5785,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.48.1': + '@typescript-eslint/scope-manager@8.50.0': dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 - '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.48.1': {} + '@typescript-eslint/types@8.50.0': {} - '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.2 + semver: 7.7.3 tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.48.1': + '@typescript-eslint/visitor-keys@8.50.0': dependencies: - '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/types': 8.50.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -5903,30 +5921,30 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2))': dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.4) - '@babel/preset-env': 7.28.0(@babel/core@7.28.4) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4) - browserslist: 4.25.1 - browserslist-to-esbuild: 2.1.1(browserslist@4.25.1) - core-js: 3.45.0 + '@babel/core': 7.28.5 + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.28.5) + '@babel/preset-env': 7.28.5(@babel/core@7.28.5) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.5) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.5) + browserslist: 4.28.1 + browserslist-to-esbuild: 2.1.1(browserslist@4.28.1) + core-js: 3.47.0 magic-string: 0.30.21 regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.1 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.47 - '@swc/core': 1.14.0 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) + '@swc/core': 1.15.7 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - '@swc/helpers' @@ -5938,9 +5956,9 @@ snapshots: adm-zip@0.5.16: {} - agent-base@7.1.3: {} + agent-base@7.1.4: {} - ahooks@3.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + ahooks@3.9.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 '@types/js-cookie': 3.0.6 @@ -5948,8 +5966,8 @@ snapshots: intersection-observer: 0.12.2 js-cookie: 3.0.5 lodash: 4.17.21 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) react-fast-compare: 3.2.2 resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 @@ -5962,17 +5980,17 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-escapes@7.1.1: + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} argparse@2.0.1: {} @@ -5987,7 +6005,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 is-string: 1.1.1 @@ -5999,7 +6017,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -6009,7 +6027,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-shim-unscopables: 1.1.0 optional: true @@ -6017,7 +6035,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-shim-unscopables: 1.1.0 optional: true @@ -6026,7 +6044,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -6044,8 +6062,8 @@ snapshots: axios@1.13.2: dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.4 + follow-redirects: 1.15.11 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -6054,29 +6072,29 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 - resolve: 1.22.10 + resolve: 1.22.11 - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + '@babel/compat-data': 7.28.5 + '@babel/core': 7.28.5 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.5): dependencies: - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) - core-js-compat: 3.44.0 + '@babel/core': 7.28.5 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5) + core-js-compat: 3.47.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.5): dependencies: - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5) transitivePeerDependencies: - supports-color @@ -6084,6 +6102,8 @@ snapshots: balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.10: {} + before-after-hook@2.2.3: {} birecord@0.1.1: {} @@ -6101,17 +6121,18 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist-to-esbuild@2.1.1(browserslist@4.25.1): + browserslist-to-esbuild@2.1.1(browserslist@4.28.1): dependencies: - browserslist: 4.25.1 + browserslist: 4.28.1 meow: 13.2.0 - browserslist@4.25.1: + browserslist@4.28.1: dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.173 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) + baseline-browser-mapping: 2.9.10 + caniuse-lite: 1.0.30001760 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-from@1.1.2: {} @@ -6138,7 +6159,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001760: {} ccount@2.0.1: {} @@ -6173,7 +6194,7 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-truncate@5.1.0: + cli-truncate@5.1.1: dependencies: slice-ansi: 7.1.2 string-width: 8.1.0 @@ -6210,13 +6231,13 @@ snapshots: convert-source-map@2.0.0: {} - cookie@1.0.2: {} + cookie@1.1.1: {} - core-js-compat@3.44.0: + core-js-compat@3.47.0: dependencies: - browserslist: 4.25.1 + browserslist: 4.28.1 - core-js@3.45.0: {} + core-js@3.47.0: {} cosmiconfig@7.1.0: dependencies: @@ -6287,7 +6308,7 @@ snapshots: dependencies: ms: 2.1.3 - decode-named-character-reference@1.1.0: + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -6345,19 +6366,19 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.173: {} + electron-to-chromium@1.5.267: {} - emoji-regex@10.5.0: {} + emoji-regex@10.6.0: {} entities@4.5.0: {} environment@1.1.0: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - es-abstract@1.24.0: + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -6467,45 +6488,46 @@ snapshots: es6-iterator: 2.0.3 es6-symbol: 3.1.4 - esbuild@0.25.4: + esbuild@0.27.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.4 - '@esbuild/android-arm': 0.25.4 - '@esbuild/android-arm64': 0.25.4 - '@esbuild/android-x64': 0.25.4 - '@esbuild/darwin-arm64': 0.25.4 - '@esbuild/darwin-x64': 0.25.4 - '@esbuild/freebsd-arm64': 0.25.4 - '@esbuild/freebsd-x64': 0.25.4 - '@esbuild/linux-arm': 0.25.4 - '@esbuild/linux-arm64': 0.25.4 - '@esbuild/linux-ia32': 0.25.4 - '@esbuild/linux-loong64': 0.25.4 - '@esbuild/linux-mips64el': 0.25.4 - '@esbuild/linux-ppc64': 0.25.4 - '@esbuild/linux-riscv64': 0.25.4 - '@esbuild/linux-s390x': 0.25.4 - '@esbuild/linux-x64': 0.25.4 - '@esbuild/netbsd-arm64': 0.25.4 - '@esbuild/netbsd-x64': 0.25.4 - '@esbuild/openbsd-arm64': 0.25.4 - '@esbuild/openbsd-x64': 0.25.4 - '@esbuild/sunos-x64': 0.25.4 - '@esbuild/win32-arm64': 0.25.4 - '@esbuild/win32-ia32': 0.25.4 - '@esbuild/win32-x64': 0.25.4 + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.10.1 + get-tsconfig: 4.13.0 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 @@ -6514,58 +6536,58 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color optional: true - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.10.1 + get-tsconfig: 4.13.0 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/types': 8.50.0 comment-parser: 1.4.1 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.1.1 - semver: 7.7.2 + semver: 7.7.3 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6574,9 +6596,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6588,122 +6610,122 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color optional: true - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.3): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4): dependencies: - eslint: 9.39.1(jiti@2.6.1) - prettier: 3.7.3 + eslint: 9.39.2(jiti@2.6.1) + prettier: 3.7.4 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-dom@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - eslint: 9.39.1(jiti@2.6.1) + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.1.13 - zod-validation-error: 4.0.2(zod@4.1.13) + zod: 4.2.1 + zod-validation-error: 4.0.2(zod@4.2.1) transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-react-web-api@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.11 - '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.13 + '@eslint-react/shared': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 9.39.1(jiti@2.6.1) - is-immutable-type: 5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + is-immutable-type: 5.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) string-ts: 2.3.1 ts-api-utils: 2.1.0(typescript@5.9.3) ts-pattern: 5.9.0 @@ -6711,11 +6733,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -6726,17 +6748,17 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.1(jiti@2.6.1): + eslint@9.39.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.1 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.6 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -6848,14 +6870,14 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + follow-redirects@1.15.11: {} for-each@0.3.5: dependencies: is-callable: 1.2.7 optional: true - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -6867,12 +6889,12 @@ snapshots: dependencies: fetch-blob: 3.2.0 - foxact@0.2.49(react@19.2.0): + foxact@0.2.49(react@19.2.3): dependencies: client-only: 0.0.1 server-only: 0.0.1 optionalDependencies: - react: 19.2.0 + react: 19.2.3 fsevents@2.3.3: optional: true @@ -6892,6 +6914,9 @@ snapshots: functions-have-names@1.2.3: optional: true + generator-function@2.0.1: + optional: true + gensync@1.0.0-beta.2: {} get-east-asian-width@1.4.0: {} @@ -6921,7 +6946,7 @@ snapshots: get-intrinsic: 1.3.0 optional: true - get-tsconfig@4.10.1: + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6933,7 +6958,7 @@ snapshots: dependencies: minimatch: 10.1.1 minipass: 7.1.2 - path-scurry: 2.0.0 + path-scurry: 2.0.1 globals@14.0.0: {} @@ -6947,8 +6972,6 @@ snapshots: gopd@1.2.0: {} - graphemer@1.4.0: {} - has-bigints@1.1.0: optional: true @@ -6988,9 +7011,9 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - style-to-js: 1.1.16 + style-to-js: 1.1.21 unist-util-position: 5.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -7016,14 +7039,14 @@ snapshots: https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.3 + agent-base: 7.1.4 debug: 4.4.3 transitivePeerDependencies: - supports-color husky@9.1.7: {} - i18next@25.7.1(typescript@5.9.3): + i18next@25.7.3(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 optionalDependencies: @@ -7033,7 +7056,7 @@ snapshots: ignore@7.0.5: {} - immutable@5.1.2: {} + immutable@5.1.4: {} import-fresh@3.3.1: dependencies: @@ -7042,7 +7065,7 @@ snapshots: imurmurhash@0.1.4: {} - inline-style-parser@0.2.4: {} + inline-style-parser@0.2.7: {} internal-slot@1.1.0: dependencies: @@ -7091,7 +7114,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 is-callable@1.2.7: optional: true @@ -7126,9 +7149,10 @@ snapshots: dependencies: get-east-asian-width: 1.4.0 - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 + generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -7140,10 +7164,10 @@ snapshots: is-hexadecimal@2.0.1: {} - is-immutable-type@5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + is-immutable-type@5.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) ts-declaration-location: 1.0.7(typescript@5.9.3) typescript: 5.9.3 @@ -7231,8 +7255,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.0.2: {} - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -7271,11 +7293,11 @@ snapshots: nano-spawn: 2.0.0 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.8.1 + yaml: 2.8.2 listr2@9.0.5: dependencies: - cli-truncate: 5.1.0 + cli-truncate: 5.1.1 colorette: 2.0.20 eventemitter3: 5.0.1 log-update: 6.1.0 @@ -7286,7 +7308,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.21: {} + lodash-es@4.17.22: {} lodash.debounce@4.0.8: {} @@ -7296,10 +7318,10 @@ snapshots: log-update@6.1.0: dependencies: - ansi-escapes: 7.1.1 + ansi-escapes: 7.2.0 cli-cursor: 5.0.0 slice-ansi: 7.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrap-ansi: 9.0.2 longest-streak@3.1.0: {} @@ -7312,7 +7334,7 @@ snapshots: dependencies: tslib: 2.8.1 - lru-cache@11.1.0: {} + lru-cache@11.2.4: {} lru-cache@5.1.1: dependencies: @@ -7334,7 +7356,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -7371,7 +7393,7 @@ snapshots: parse-entities: 4.0.2 stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -7389,9 +7411,9 @@ snapshots: mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 - unist-util-is: 6.0.0 + unist-util-is: 6.0.1 - mdast-util-to-hast@13.2.0: + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -7434,7 +7456,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -7509,7 +7531,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -7547,7 +7569,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 debug: 4.4.3 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -7629,11 +7651,11 @@ snapshots: monaco-types: 0.1.0 monaco-worker-manager: 2.0.1(monaco-editor@0.55.1) path-browserify: 1.0.1 - prettier: 3.7.3 + prettier: 3.7.4 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-uri: 3.1.0 - yaml: 2.8.1 + yaml: 2.8.2 ms@2.1.3: {} @@ -7643,7 +7665,7 @@ snapshots: nanoid@5.1.6: {} - napi-postinstall@0.3.3: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -7665,7 +7687,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-releases@2.0.19: {} + node-releases@2.0.27: {} object-assign@4.1.1: {} @@ -7689,7 +7711,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 optional: true @@ -7697,7 +7719,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 optional: true object.values@1.2.1: @@ -7749,7 +7771,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -7757,7 +7779,7 @@ snapshots: parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -7769,9 +7791,9 @@ snapshots: path-parse@1.0.7: {} - path-scurry@2.0.0: + path-scurry@2.0.1: dependencies: - lru-cache: 11.1.0 + lru-cache: 11.2.4 minipass: 7.1.2 path-type@4.0.0: {} @@ -7799,7 +7821,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.7.3: {} + prettier@3.7.4: {} prop-types@15.8.1: dependencies: @@ -7813,38 +7835,38 @@ snapshots: punycode@2.3.1: {} - react-dom@19.2.0(react@19.2.0): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 scheduler: 0.27.0 - react-error-boundary@6.0.0(react@19.2.0): + react-error-boundary@6.0.0(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 - react: 19.2.0 + react: 19.2.3 react-fast-compare@3.2.2: {} - react-hook-form@7.67.0(react@19.2.0): + react-hook-form@7.68.0(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 - react-i18next@16.3.5(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.7.1(typescript@5.9.3) - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) + i18next: 25.7.3(typescript@5.9.3) + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: - react-dom: 19.2.0(react@19.2.0) + react-dom: 19.2.3(react@19.2.3) typescript: 5.9.3 react-is@16.13.1: {} - react-is@19.2.0: {} + react-is@19.2.3: {} - react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.3): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -7852,8 +7874,8 @@ snapshots: devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.0 - react: 19.2.0 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -7862,29 +7884,29 @@ snapshots: transitivePeerDependencies: - supports-color - react-router@7.10.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - cookie: 1.0.2 - react: 19.2.0 - set-cookie-parser: 2.7.1 + cookie: 1.1.1 + react: 19.2.3 + set-cookie-parser: 2.7.2 optionalDependencies: - react-dom: 19.2.0(react@19.2.0) + react-dom: 19.2.3(react@19.2.3) - react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - react-virtuoso@4.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-virtuoso@4.17.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - react@19.2.0: {} + react@19.2.3: {} readdirp@4.1.2: {} @@ -7892,7 +7914,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -7900,7 +7922,7 @@ snapshots: which-builtin-type: 1.2.1 optional: true - regenerate-unicode-properties@10.2.0: + regenerate-unicode-properties@10.2.2: dependencies: regenerate: 1.4.2 @@ -7918,20 +7940,20 @@ snapshots: set-function-name: 2.0.2 optional: true - regexpu-core@6.2.0: + regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 - regenerate-unicode-properties: 10.2.0 + regenerate-unicode-properties: 10.2.2 regjsgen: 0.8.0 - regjsparser: 0.12.0 + regjsparser: 0.13.0 unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.2.0 + unicode-match-property-value-ecmascript: 2.2.1 regjsgen@0.8.0: {} - regjsparser@0.12.0: + regjsparser@0.13.0: dependencies: - jsesc: 3.0.2 + jsesc: 3.1.0 remark-parse@11.0.0: dependencies: @@ -7946,7 +7968,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 + mdast-util-to-hast: 13.2.1 unified: 11.0.5 vfile: 6.0.3 @@ -7956,7 +7978,7 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 @@ -7969,30 +7991,32 @@ snapshots: rfdc@1.4.1: {} - rollup@4.46.2: + rollup@4.53.5: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.2 - '@rollup/rollup-android-arm64': 4.46.2 - '@rollup/rollup-darwin-arm64': 4.46.2 - '@rollup/rollup-darwin-x64': 4.46.2 - '@rollup/rollup-freebsd-arm64': 4.46.2 - '@rollup/rollup-freebsd-x64': 4.46.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 - '@rollup/rollup-linux-arm-musleabihf': 4.46.2 - '@rollup/rollup-linux-arm64-gnu': 4.46.2 - '@rollup/rollup-linux-arm64-musl': 4.46.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 - '@rollup/rollup-linux-ppc64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-musl': 4.46.2 - '@rollup/rollup-linux-s390x-gnu': 4.46.2 - '@rollup/rollup-linux-x64-gnu': 4.46.2 - '@rollup/rollup-linux-x64-musl': 4.46.2 - '@rollup/rollup-win32-arm64-msvc': 4.46.2 - '@rollup/rollup-win32-ia32-msvc': 4.46.2 - '@rollup/rollup-win32-x64-msvc': 4.46.2 + '@rollup/rollup-android-arm-eabi': 4.53.5 + '@rollup/rollup-android-arm64': 4.53.5 + '@rollup/rollup-darwin-arm64': 4.53.5 + '@rollup/rollup-darwin-x64': 4.53.5 + '@rollup/rollup-freebsd-arm64': 4.53.5 + '@rollup/rollup-freebsd-x64': 4.53.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.5 + '@rollup/rollup-linux-arm-musleabihf': 4.53.5 + '@rollup/rollup-linux-arm64-gnu': 4.53.5 + '@rollup/rollup-linux-arm64-musl': 4.53.5 + '@rollup/rollup-linux-loong64-gnu': 4.53.5 + '@rollup/rollup-linux-ppc64-gnu': 4.53.5 + '@rollup/rollup-linux-riscv64-gnu': 4.53.5 + '@rollup/rollup-linux-riscv64-musl': 4.53.5 + '@rollup/rollup-linux-s390x-gnu': 4.53.5 + '@rollup/rollup-linux-x64-gnu': 4.53.5 + '@rollup/rollup-linux-x64-musl': 4.53.5 + '@rollup/rollup-openharmony-arm64': 4.53.5 + '@rollup/rollup-win32-arm64-msvc': 4.53.5 + '@rollup/rollup-win32-ia32-msvc': 4.53.5 + '@rollup/rollup-win32-x64-gnu': 4.53.5 + '@rollup/rollup-win32-x64-msvc': 4.53.5 fsevents: 2.3.3 safe-array-concat@1.1.3: @@ -8017,10 +8041,10 @@ snapshots: is-regex: 1.2.1 optional: true - sass@1.94.2: + sass@1.97.0: dependencies: chokidar: 4.0.3 - immutable: 5.1.2 + immutable: 5.1.4 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.1 @@ -8031,11 +8055,11 @@ snapshots: semver@6.3.1: {} - semver@7.7.2: {} + semver@7.7.3: {} server-only@0.0.1: {} - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -8104,7 +8128,7 @@ snapshots: slice-ansi@7.1.2: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 snake-case@3.0.4: @@ -8141,14 +8165,14 @@ snapshots: string-width@7.2.0: dependencies: - emoji-regex: 10.5.0 + emoji-regex: 10.6.0 get-east-asian-width: 1.4.0 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@8.1.0: dependencies: get-east-asian-width: 1.4.0 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string.prototype.trim@1.2.10: dependencies: @@ -8156,7 +8180,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 optional: true @@ -8181,22 +8205,22 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-bom@3.0.0: optional: true strip-json-comments@3.1.1: {} - style-to-js@1.1.16: + style-to-js@1.1.21: dependencies: - style-to-object: 1.0.8 + style-to-object: 1.0.14 - style-to-object@1.0.8: + style-to-object@1.0.14: dependencies: - inline-style-parser: 0.2.4 + inline-style-parser: 0.2.7 stylis@4.2.0: {} @@ -8208,11 +8232,11 @@ snapshots: svg-parser@2.0.4: {} - swr@2.3.7(react@19.2.0): + swr@2.3.8(react@19.2.3): dependencies: dequal: 2.0.3 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) synckit@0.11.11: dependencies: @@ -8228,13 +8252,13 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210: + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288: dependencies: '@tauri-apps/api': 2.9.1 terser@5.44.1: dependencies: - '@jridgewell/source-map': 0.3.6 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -8325,13 +8349,13 @@ snapshots: types-pac@1.0.3: {} - typescript-eslint@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8357,11 +8381,11 @@ snapshots: unicode-match-property-ecmascript@2.0.0: dependencies: unicode-canonical-property-names-ecmascript: 2.0.1 - unicode-property-aliases-ecmascript: 2.1.0 + unicode-property-aliases-ecmascript: 2.2.0 - unicode-match-property-value-ecmascript@2.2.0: {} + unicode-match-property-value-ecmascript@2.2.1: {} - unicode-property-aliases-ecmascript@2.1.0: {} + unicode-property-aliases-ecmascript@2.2.0: {} unified@11.0.5: dependencies: @@ -8373,7 +8397,7 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-util-is@6.0.0: + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -8385,22 +8409,22 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@6.0.1: + unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 - unist-util-is: 6.0.0 + unist-util-is: 6.0.1 unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 universal-user-agent@6.0.1: {} unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.3 + napi-postinstall: 0.3.4 optionalDependencies: '@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1 @@ -8422,9 +8446,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.1.3(browserslist@4.25.1): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.25.1 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -8432,11 +8456,11 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.0): + use-sync-external-store@1.6.0(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 - vfile-message@4.0.2: + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 @@ -8444,34 +8468,34 @@ snapshots: vfile@6.0.3: dependencies: '@types/unist': 3.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.53.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2)): dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + '@rollup/pluginutils': 5.3.0(rollup@4.53.5) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1): + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(sass@1.97.0)(terser@5.44.1)(yaml@2.8.2): dependencies: - esbuild: 0.25.4 + esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.2 + rollup: 4.53.5 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 24.10.4 fsevents: 2.3.3 jiti: 2.6.1 - sass: 1.94.2 + sass: 1.97.0 terser: 5.44.1 - yaml: 2.8.1 + yaml: 2.8.2 void-elements@3.1.0: {} @@ -8507,7 +8531,7 @@ snapshots: is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 @@ -8543,9 +8567,9 @@ snapshots: wrap-ansi@9.0.2: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 7.2.0 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrappy@1.0.2: {} @@ -8555,14 +8579,14 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.1.13): + zod-validation-error@4.0.2(zod@4.2.1): dependencies: - zod: 4.1.13 + zod: 4.2.1 - zod@4.1.13: {} + zod@4.2.1: {} zwitch@2.0.4: {} diff --git a/clash-verge-rev/pnpm-workspace.yaml b/clash-verge-rev/pnpm-workspace.yaml deleted file mode 100644 index 97a7be15ce..0000000000 --- a/clash-verge-rev/pnpm-workspace.yaml +++ /dev/null @@ -1,7 +0,0 @@ -onlyBuiltDependencies: - - "@parcel/watcher" - - "@swc/core" - - core-js - - es5-ext - - esbuild - - unrs-resolver diff --git a/clash-verge-rev/renovate.json b/clash-verge-rev/renovate.json deleted file mode 100644 index f3d1b10763..0000000000 --- a/clash-verge-rev/renovate.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "extends": ["config:recommended", ":disableDependencyDashboard"], - "baseBranches": ["dev"], - "enabledManagers": ["cargo", "npm", "github-actions"], - "labels": ["dependencies"], - "ignorePaths": [ - "**/node_modules/**", - "**/bower_components/**", - "**/vendor/**", - "**/__tests__/**", - "**/test/**", - "**/tests/**", - "**/__fixtures__/**", - "**/crate/**", - "shared/**" - ], - "rangeStrategy": "bump", - "packageRules": [ - { - "semanticCommitType": "chore", - "matchPackageNames": ["*"] - }, - { - "description": "Disable node/pnpm version updates", - "matchPackageNames": ["node", "pnpm"], - "matchDepTypes": ["engines", "packageManager"], - "enabled": false - }, - { - "description": "Group all cargo dependencies into a single PR", - "matchManagers": ["cargo"], - "groupName": "cargo dependencies" - }, - { - "description": "Group all npm dependencies into a single PR", - "matchManagers": ["npm"], - "groupName": "npm dependencies" - }, - { - "description": "Group all GitHub Actions updates into a single PR", - "matchManagers": ["github-actions"], - "groupName": "github actions" - } - ], - "postUpdateOptions": ["pnpmDedupe", "updateCargoLock"], - "ignoreDeps": ["criterion"] -} diff --git a/clash-verge-rev/renovate.json5 b/clash-verge-rev/renovate.json5 new file mode 100644 index 0000000000..d7fc1ab5f6 --- /dev/null +++ b/clash-verge-rev/renovate.json5 @@ -0,0 +1,50 @@ +{ + extends: ["config:recommended", ":disableDependencyDashboard"], + baseBranches: ["dev"], + enabledManagers: ["cargo", "npm", "github-actions"], + labels: ["dependencies"], + ignorePaths: [ + "**/node_modules/**", + "**/bower_components/**", + "**/vendor/**", + "**/__tests__/**", + "**/test/**", + "**/tests/**", + "**/__fixtures__/**", + "shared/**", + ], + rangeStrategy: "replace", + packageRules: [ + { + matchUpdateTypes: ["patch"], + automerge: true, + }, + { + semanticCommitType: "chore", + matchPackageNames: ["*"], + }, + { + description: "Disable node/pnpm version updates", + matchPackageNames: ["node", "pnpm"], + matchDepTypes: ["engines", "packageManager"], + enabled: false, + }, + { + description: "Group all cargo dependencies into a single PR", + matchManagers: ["cargo"], + groupName: "cargo dependencies", + }, + { + description: "Group all npm dependencies into a single PR", + matchManagers: ["npm"], + groupName: "npm dependencies", + }, + { + description: "Group all GitHub Actions updates into a single PR", + matchManagers: ["github-actions"], + groupName: "github actions", + }, + ], + postUpdateOptions: ["pnpmDedupe"], + ignoreDeps: ["criterion"], +} diff --git a/clash-verge-rev/scripts/cleanup-unused-i18n.mjs b/clash-verge-rev/scripts/cleanup-unused-i18n.mjs index ce8af9c3d7..45e7a9be7a 100644 --- a/clash-verge-rev/scripts/cleanup-unused-i18n.mjs +++ b/clash-verge-rev/scripts/cleanup-unused-i18n.mjs @@ -77,9 +77,9 @@ const IGNORED_KEY_PREFIXES = new Set([ const NOTICE_METHOD_NAMES = new Set(["success", "error", "info", "warning"]); const NOTICE_SERVICE_IDENTIFIERS = new Set([ - "@/services/noticeService", - "./noticeService", - "../services/noticeService", + "@/services/notice-service", + "./notice-service", + "../services/notice-service", ]); const WHITELIST_KEYS = new Set([ diff --git a/clash-verge-rev/scripts/portable-fixed-webview2.mjs b/clash-verge-rev/scripts/portable-fixed-webview2.mjs index 99bbbc62df..1a62be3f40 100644 --- a/clash-verge-rev/scripts/portable-fixed-webview2.mjs +++ b/clash-verge-rev/scripts/portable-fixed-webview2.mjs @@ -3,7 +3,7 @@ import fsp from "fs/promises"; import { createRequire } from "module"; import path from "path"; -import { getOctokit, context } from "@actions/github"; +import { context, getOctokit } from "@actions/github"; import AdmZip from "adm-zip"; const target = process.argv.slice(2)[0]; @@ -50,9 +50,9 @@ async function resolvePortable() { zip.addLocalFolder( path.join( releaseDir, - `Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`, + `Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`, ), - `Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`, + `Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`, ); zip.addLocalFolder(configDir, ".config"); diff --git a/clash-verge-rev/scripts/prebuild.mjs b/clash-verge-rev/scripts/prebuild.mjs index 21f0da7cc4..7325d571c1 100644 --- a/clash-verge-rev/scripts/prebuild.mjs +++ b/clash-verge-rev/scripts/prebuild.mjs @@ -576,7 +576,7 @@ const resolveServicePermission = async () => { }; // ======================= -// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy) +// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback) // ======================= const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`; const resolveService = () => { @@ -624,11 +624,6 @@ const resolveEnableLoopback = () => file: "enableLoopback.exe", downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`, }); -const resolveWinSysproxy = () => - resolveResource({ - file: "sysproxy.exe", - downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`, - }); const resolveSetDnsScript = () => resolveResource({ @@ -676,12 +671,6 @@ const tasks = [ retry: 5, unixOnly: platform === "linux" || platform === "darwin", }, - { - name: "windows-sysproxy", - func: resolveWinSysproxy, - retry: 5, - winOnly: true, - }, { name: "set_dns_script", func: resolveSetDnsScript, diff --git a/clash-verge-rev/src-tauri/Cargo.toml b/clash-verge-rev/src-tauri/Cargo.toml index ac1810f2a8..52912a82fd 100755 --- a/clash-verge-rev/src-tauri/Cargo.toml +++ b/clash-verge-rev/src-tauri/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "clash-verge" -version = "2.4.4" +version = "2.4.4-rc.1" description = "clash verge" authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] license = "GPL-3.0-only" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" default-run = "clash-verge" build = "build.rs" -edition = { workspace = true } -rust-version = { workspace = true } +edition = "2024" +rust-version = "1.91" [lib] name = "app_lib" @@ -22,6 +22,7 @@ tauri-dev = ["clash-verge-logging/tauri-dev"] tokio-trace = ["console-subscriber"] clippy = ["tauri/test"] tracing = [] +tracing-full = [] [package.metadata.bundle] identifier = "io.github.clash-verge-rev.clash-verge-rev" @@ -93,7 +94,7 @@ tauri-plugin-devtools = { version = "2.0.1" } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" } clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" } async-trait = "0.1.89" -clash_verge_service_ipc = { version = "2.0.21", features = [ +clash_verge_service_ipc = { version = "2.0.26", features = [ "client", ], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" } arc-swap = "1.7.1" diff --git a/clash-verge-rev/src-tauri/locales/en.yml b/clash-verge-rev/src-tauri/locales/en.yml index bd2ef91fd1..fbfee9cb3a 100644 --- a/clash-verge-rev/src-tauri/locales/en.yml +++ b/clash-verge-rev/src-tauri/locales/en.yml @@ -25,7 +25,9 @@ notifications: title: Application Hidden body: Clash Verge is running in the background. service: - adminPrompt: Installing the service requires administrator privileges. + adminInstallPrompt: Installing the service requires administrator privileges. + adminUninstallPrompt: Uninstalling the service requires administrator privileges. + tray: dashboard: Dashboard ruleMode: Rule Mode diff --git a/clash-verge-rev/src-tauri/locales/zh.yml b/clash-verge-rev/src-tauri/locales/zh.yml index dd1fa4b537..59d2aa512b 100644 --- a/clash-verge-rev/src-tauri/locales/zh.yml +++ b/clash-verge-rev/src-tauri/locales/zh.yml @@ -25,7 +25,8 @@ notifications: title: 应用已隐藏 body: Clash Verge 正在后台运行。 service: - adminPrompt: 安装服务需要管理员权限 + adminInstallPrompt: 安装 Clash Verge 服务需要管理员权限 + adminUninstallPrompt: 卸载 Clash Verge 服务需要管理员权限 tray: dashboard: 仪表板 ruleMode: 规则模式 diff --git a/clash-verge-rev/src-tauri/locales/zhtw.yml b/clash-verge-rev/src-tauri/locales/zhtw.yml index aaeec6c91f..2291530f0e 100644 --- a/clash-verge-rev/src-tauri/locales/zhtw.yml +++ b/clash-verge-rev/src-tauri/locales/zhtw.yml @@ -25,7 +25,8 @@ notifications: title: 應用已隱藏 body: Clash Verge 正在背景執行。 service: - adminPrompt: 安裝服務需要管理員權限 + adminInstallPrompt: 安裝服務需要管理員權限 + adminUninstallPrompt: 卸载服務需要管理員權限 tray: dashboard: 儀表板 ruleMode: 規則模式 diff --git a/clash-verge-rev/src-tauri/rustfmt.toml b/clash-verge-rev/src-tauri/rustfmt.toml index 7e141a0b8a..d9fae489a1 100644 --- a/clash-verge-rev/src-tauri/rustfmt.toml +++ b/clash-verge-rev/src-tauri/rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 100 +max_width = 120 hard_tabs = false tab_spaces = 4 newline_style = "Auto" diff --git a/clash-verge-rev/src-tauri/src/cmd/app.rs b/clash-verge-rev/src-tauri/src/cmd/app.rs index 1e0f294227..9a2fe39ab2 100644 --- a/clash-verge-rev/src-tauri/src/cmd/app.rs +++ b/clash-verge-rev/src-tauri/src/cmd/app.rs @@ -89,10 +89,7 @@ pub fn get_portable_flag() -> bool { /// 获取应用目录 #[tauri::command] pub fn get_app_dir() -> CmdResult { - let app_home_dir = dirs::app_home_dir() - .stringify_err()? - .to_string_lossy() - .into(); + let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into(); Ok(app_home_dir) } @@ -105,10 +102,7 @@ pub fn get_auto_launch_status() -> CmdResult { /// 下载图标缓存 #[tauri::command] pub async fn download_icon_cache(url: String, name: String) -> CmdResult { - let icon_cache_dir = dirs::app_home_dir() - .stringify_err()? - .join("icons") - .join("cache"); + let icon_cache_dir = dirs::app_home_dir().stringify_err()?.join("icons").join("cache"); let icon_path = icon_cache_dir.join(name.as_str()); if icon_path.exists() { @@ -134,9 +128,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult let content = response.bytes().await.stringify_err()?; let is_html = content.len() > 15 - && (content.starts_with(b" CmdResult<()> { /// Export local backup to a user selected destination #[tauri::command] pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> { - feat::export_local_backup(filename, destination) - .await - .stringify_err() + feat::export_local_backup(filename, destination).await.stringify_err() } diff --git a/clash-verge-rev/src-tauri/src/cmd/clash.rs b/clash-verge-rev/src-tauri/src/cmd/clash.rs index 830eb2c721..1a8a1282e5 100644 --- a/clash-verge-rev/src-tauri/src/cmd/clash.rs +++ b/clash-verge-rev/src-tauri/src/cmd/clash.rs @@ -46,26 +46,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult> match CoreManager::global().change_core(&clash_core).await { Ok(_) => { - logging_error!( - Type::Core, - Config::profiles().await.latest_arc().save_file().await - ); + logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await); // 切换内核后重启内核 match CoreManager::global().restart_core().await { Ok(_) => { - logging!( - info, - Type::Core, - "core changed and restarted to {clash_core}" - ); + logging!(info, Type::Core, "core changed and restarted to {clash_core}"); handle::Handle::notice_message("config_core::change_success", clash_core); handle::Handle::refresh_clash(); Ok(None) } Err(err) => { - let error_msg: String = - format!("Core changed but failed to restart: {err}").into(); + let error_msg: String = format!("Core changed but failed to restart: {err}").into(); handle::Handle::notice_message("config_core::change_error", error_msg.clone()); logging!(error, Type::Core, "{error_msg}"); Ok(Some(error_msg)) @@ -94,10 +86,7 @@ pub async fn start_core() -> CmdResult { /// 关闭核心 #[tauri::command] pub async fn stop_core() -> CmdResult { - logging_error!( - Type::Core, - Config::profiles().await.latest_arc().save_file().await - ); + logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await); let result = CoreManager::global().stop_core().await.stringify_err(); if result.is_ok() { handle::Handle::refresh_clash(); @@ -108,10 +97,7 @@ pub async fn stop_core() -> CmdResult { /// 重启核心 #[tauri::command] pub async fn restart_core() -> CmdResult { - logging_error!( - Type::Core, - Config::profiles().await.latest_arc().save_file().await - ); + logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await); let result = CoreManager::global().restart_core().await.stringify_err(); if result.is_ok() { handle::Handle::refresh_clash(); @@ -140,9 +126,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { use tokio::fs; // 获取DNS配置文件路径 - let dns_path = dirs::app_home_dir() - .stringify_err()? - .join(constants::files::DNS_CONFIG); + let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); // 保存DNS配置到文件 let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?; @@ -157,9 +141,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { pub async fn apply_dns_config(apply: bool) -> CmdResult { if apply { // 读取DNS配置文件 - let dns_path = dirs::app_home_dir() - .stringify_err()? - .join(constants::files::DNS_CONFIG); + let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); if !dns_path.exists() { logging!(warn, Type::Config, "DNS config file not found"); @@ -171,10 +153,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { })?; // 解析DNS配置 - let patch_config = serde_yaml_ng::from_str::(&dns_yaml) - .stringify_err_log(|e| { - logging!(error, Type::Config, "Failed to parse DNS config: {e}"); - })?; + let patch_config = serde_yaml_ng::from_str::(&dns_yaml).stringify_err_log(|e| { + logging!(error, Type::Config, "Failed to parse DNS config: {e}"); + })?; logging!(info, Type::Config, "Applying DNS config from file"); @@ -194,35 +175,25 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { })?; // 应用新配置 - CoreManager::global() - .update_config() - .await - .stringify_err_log(|err| { - let err = format!("Failed to apply config with DNS: {err}"); - logging!(error, Type::Config, "{err}"); - })?; + CoreManager::global().update_config().await.stringify_err_log(|err| { + let err = format!("Failed to apply config with DNS: {err}"); + logging!(error, Type::Config, "{err}"); + })?; logging!(info, Type::Config, "DNS config successfully applied"); } else { // 当关闭DNS设置时,重新生成配置(不加载DNS配置文件) - logging!( - info, - Type::Config, - "DNS settings disabled, regenerating config" - ); + logging!(info, Type::Config, "DNS settings disabled, regenerating config"); Config::generate().await.stringify_err_log(|err| { let err = format!("Failed to regenerate config: {err}"); logging!(error, Type::Config, "{err}"); })?; - CoreManager::global() - .update_config() - .await - .stringify_err_log(|err| { - let err = format!("Failed to apply regenerated config: {err}"); - logging!(error, Type::Config, "{err}"); - })?; + CoreManager::global().update_config().await.stringify_err_log(|err| { + let err = format!("Failed to apply regenerated config: {err}"); + logging!(error, Type::Config, "{err}"); + })?; logging!(info, Type::Config, "Config regenerated successfully"); } @@ -236,9 +207,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { pub fn check_dns_config_exists() -> CmdResult { use crate::utils::dirs; - let dns_path = dirs::app_home_dir() - .stringify_err()? - .join(constants::files::DNS_CONFIG); + let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); Ok(dns_path.exists()) } @@ -249,9 +218,7 @@ pub async fn get_dns_config_content() -> CmdResult { use crate::utils::dirs; use tokio::fs; - let dns_path = dirs::app_home_dir() - .stringify_err()? - .join(constants::files::DNS_CONFIG); + let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); if !fs::try_exists(&dns_path).await.stringify_err()? { return Err("DNS config file not found".into()); @@ -279,9 +246,6 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> { #[tauri::command] pub async fn get_clash_logs() -> CmdResult> { - let logs = CoreManager::global() - .get_clash_logs() - .await - .unwrap_or_default(); + let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default(); Ok(logs) } diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs index 7ac98dc6c7..6a5707bec1 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs @@ -12,9 +12,12 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem { let client_with_cookies = match Client::builder() .use_rustls_tls() - .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") + .user_agent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + ) .cookie_provider(Arc::clone(&cookie_store)) - .build() { + .build() + { Ok(client) => client, Err(e) => { logging!( @@ -59,8 +62,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem { }; } - let url = - format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}"); + let url = format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}"); let token_result = match client_with_cookies.get(&url).send().await { Ok(response) => match response.text().await { @@ -85,21 +87,14 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem { }; } - let region = match client_with_cookies - .get("https://ani.gamer.com.tw/") - .send() - .await - { + let region = match client_with_cookies.get("https://ani.gamer.com.tw/").send().await { Ok(response) => match response.text().await { Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) { - Ok(region_re) => region_re - .captures(&body) - .and_then(|caps| caps.get(1)) - .map(|m| { - let country_code = m.as_str(); - let emoji = country_code_to_emoji(country_code); - format!("{emoji}{country_code}") - }), + Ok(region_re) => region_re.captures(&body).and_then(|caps| caps.get(1)).map(|m| { + let country_code = m.as_str(); + let emoji = country_code_to_emoji(country_code); + format!("{emoji}{country_code}") + }), Err(e) => { logging!( error, diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs index 084f44e90b..1686d37278 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs @@ -9,8 +9,7 @@ use super::utils::{country_code_to_emoji, get_local_date_string}; #[allow(clippy::cognitive_complexity)] pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem { let device_api_url = "https://disney.api.edge.bamgrid.com/devices"; - let auth_header = - "Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84"; + let auth_header = "Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84"; let device_req_body = serde_json::json!({ "deviceFamily": "browser", @@ -39,12 +38,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem { let device_response = match device_result { Ok(response) => response, Err(e) => { - logging!( - error, - Type::Network, - "Failed to get Disney+ device response: {}", - e - ); + logging!(error, Type::Network, "Failed to get Disney+ device response: {}", e); return UnlockItem { name: "Disney+".to_string(), status: "Failed (Network Connection)".to_string(), @@ -120,18 +114,12 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem { } }; let token_body = [ - ( - "grant_type", - "urn:ietf:params:oauth:grant-type:token-exchange", - ), + ("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"), ("latitude", "0"), ("longitude", "0"), ("platform", "browser"), ("subject_token", assertion_str.as_str()), - ( - "subject_token_type", - "urn:bamtech:params:oauth:token-type:device", - ), + ("subject_token_type", "urn:bamtech:params:oauth:token-type:device"), ]; let token_result = client @@ -154,12 +142,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem { let token_response = match token_result { Ok(response) => response, Err(e) => { - logging!( - error, - Type::Network, - "Failed to get Disney+ token response: {}", - e - ); + logging!(error, Type::Network, "Failed to get Disney+ token response: {}", e); return UnlockItem { name: "Disney+".to_string(), status: "Failed (Network Connection)".to_string(), @@ -264,12 +247,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem { let graphql_response = match graphql_result { Ok(response) => response, Err(e) => { - logging!( - error, - Type::Network, - "Failed to get Disney+ GraphQL response: {}", - e - ); + logging!(error, Type::Network, "Failed to get Disney+ GraphQL response: {}", e); return UnlockItem { name: "Disney+".to_string(), status: "Failed (Network Connection)".to_string(), diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs index e9412a1fb6..93371e28a0 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs @@ -18,12 +18,7 @@ pub(super) async fn check_gemini(client: &Client) -> UnlockItem { let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) { Ok(re) => re, Err(e) => { - logging!( - error, - Type::Network, - "Failed to compile Gemini regex: {}", - e - ); + logging!(error, Type::Network, "Failed to compile Gemini regex: {}", e); return UnlockItem { name: "Gemini".to_string(), status: "Failed".to_string(), diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs index 63def9f5d8..f4aefccbd3 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs @@ -50,12 +50,7 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem { let status1 = match result1 { Ok(response) => response.status().as_u16(), Err(e) => { - logging!( - error, - Type::Network, - "Failed to get Netflix response 1: {}", - e - ); + logging!(error, Type::Network, "Failed to get Netflix response 1: {}", e); return UnlockItem { name: "Netflix".to_string(), status: "Failed".to_string(), @@ -68,12 +63,7 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem { let status2 = match result2 { Ok(response) => response.status().as_u16(), Err(e) => { - logging!( - error, - Type::Network, - "Failed to get Netflix response 2: {}", - e - ); + logging!(error, Type::Network, "Failed to get Netflix response 2: {}", e); return UnlockItem { name: "Netflix".to_string(), status: "Failed".to_string(), @@ -157,12 +147,7 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem { async fn check_netflix_cdn(client: &Client) -> UnlockItem { let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5"; - match client - .get(url) - .timeout(std::time::Duration::from_secs(30)) - .send() - .await - { + match client.get(url).timeout(std::time::Duration::from_secs(30)).send().await { Ok(response) => { if response.status().as_u16() == 403 { return UnlockItem { diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs index 978c7d7c70..ff356e8804 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs @@ -23,12 +23,7 @@ pub(super) async fn check_prime_video(client: &Client) -> UnlockItem { let response = match result { Ok(response) => response, Err(e) => { - logging!( - error, - Type::Network, - "Failed to get Prime Video response: {}", - e - ); + logging!(error, Type::Network, "Failed to get Prime Video response: {}", e); return UnlockItem { name: "Prime Video".to_string(), status: "Failed (Network Connection)".to_string(), diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs index 116335d65b..01d8dfd248 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs @@ -31,12 +31,7 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem { } } Err(e) => { - logging!( - error, - Type::Network, - "Failed to compile YouTube Premium regex: {}", - e - ); + logging!(error, Type::Network, "Failed to compile YouTube Premium regex: {}", e); } } } diff --git a/clash-verge-rev/src-tauri/src/cmd/network.rs b/clash-verge-rev/src-tauri/src/cmd/network.rs index b3d85b8ec5..3c9aee77b1 100644 --- a/clash-verge-rev/src-tauri/src/cmd/network.rs +++ b/clash-verge-rev/src-tauri/src/cmd/network.rs @@ -40,10 +40,7 @@ pub async fn get_sys_proxy() -> CmdResult { #[tauri::command] pub async fn get_auto_proxy() -> CmdResult { let auto_proxy = Autoproxy::get_auto_proxy().stringify_err()?; - let Autoproxy { - ref enable, - ref url, - } = auto_proxy; + let Autoproxy { ref enable, ref url } = auto_proxy; let mut map = Mapping::new(); map.insert("enable".into(), (*enable).into()); diff --git a/clash-verge-rev/src-tauri/src/cmd/profile.rs b/clash-verge-rev/src-tauri/src/cmd/profile.rs index 8610400668..5740237290 100644 --- a/clash-verge-rev/src-tauri/src/cmd/profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/profile.rs @@ -4,8 +4,8 @@ use crate::{ config::{ Config, IProfiles, PrfItem, PrfOption, profiles::{ - profiles_append_item_with_filedata_safe, profiles_delete_item_safe, - profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe, + profiles_append_item_with_filedata_safe, profiles_delete_item_safe, profiles_patch_item_safe, + profiles_reorder_safe, profiles_save_file_safe, }, profiles_append_item_safe, }, @@ -223,32 +223,20 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> { // 如果获取到文件路径,检查YAML语法 if let Some(file_path) = config_file_result { if !file_path.exists() { - logging!( - error, - Type::Cmd, - "目标配置文件不存在: {}", - file_path.display() - ); - handle::Handle::notice_message( - "config_validate::file_not_found", - format!("{}", file_path.display()), - ); + logging!(error, Type::Cmd, "目标配置文件不存在: {}", file_path.display()); + handle::Handle::notice_message("config_validate::file_not_found", format!("{}", file_path.display())); return Err(()); } // 超时保护 - let file_read_result = tokio::time::timeout( - Duration::from_secs(5), - tokio::fs::read_to_string(&file_path), - ) - .await; + let file_read_result = + tokio::time::timeout(Duration::from_secs(5), tokio::fs::read_to_string(&file_path)).await; match file_read_result { Ok(Ok(content)) => { - let yaml_parse_result = AsyncHandler::spawn_blocking(move || { - serde_yaml_ng::from_str::(&content) - }) - .await; + let yaml_parse_result = + AsyncHandler::spawn_blocking(move || serde_yaml_ng::from_str::(&content)) + .await; match yaml_parse_result { Ok(Ok(_)) => { @@ -257,25 +245,14 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> { } Ok(Err(err)) => { let error_msg = format!(" {err}"); - logging!( - error, - Type::Cmd, - "目标配置文件存在YAML语法错误:{}", - error_msg - ); - handle::Handle::notice_message( - "config_validate::yaml_syntax_error", - error_msg, - ); + logging!(error, Type::Cmd, "目标配置文件存在YAML语法错误:{}", error_msg); + handle::Handle::notice_message("config_validate::yaml_syntax_error", error_msg); Err(()) } Err(join_err) => { let error_msg = format!("YAML解析任务失败: {join_err}"); logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::yaml_parse_error", - error_msg, - ); + handle::Handle::notice_message("config_validate::yaml_parse_error", error_msg); Err(()) } } @@ -342,10 +319,7 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult { Ok(true) } -async fn handle_validation_failure( - error_msg: String, - current_profile: Option<&String>, -) -> CmdResult { +async fn handle_validation_failure(error_msg: String, current_profile: Option<&String>) -> CmdResult { logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg); Config::profiles().await.discard(); if let Some(prev_profile) = current_profile { @@ -373,18 +347,11 @@ async fn handle_timeout(current_profile: Option<&String>) -> CmdResult { Ok(false) } -async fn perform_config_update( - current_value: Option<&String>, - current_profile: Option<&String>, -) -> CmdResult { +async fn perform_config_update(current_value: Option<&String>, current_profile: Option<&String>) -> CmdResult { defer! { CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release); } - let update_result = tokio::time::timeout( - Duration::from_secs(30), - CoreManager::global().update_config(), - ) - .await; + let update_result = tokio::time::timeout(Duration::from_secs(30), CoreManager::global().update_config()).await; match update_result { Ok(Ok((true, _))) => handle_success(current_value).await, @@ -407,12 +374,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { let target_profile = profiles.current.as_ref(); - logging!( - info, - Type::Cmd, - "开始修改配置文件,目标profile: {:?}", - target_profile - ); + logging!(info, Type::Cmd, "开始修改配置文件,目标profile: {:?}", target_profile); // 保存当前配置,以便在验证失败时恢复 let previous_profile = Config::profiles().await.data_arc().current.clone(); @@ -426,9 +388,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release); return Ok(false); } - Config::profiles() - .await - .edit_draft(|d| d.patch_config(&profiles)); + Config::profiles().await.edit_draft(|d| d.patch_config(&profiles)); perform_config_update(target_profile, previous_profile.as_ref()).await } @@ -455,19 +415,14 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { { let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); let new_interval = new_option.update_interval; - let old_allow_auto_update = old_profile - .option - .as_ref() - .and_then(|o| o.allow_auto_update); + let old_allow_auto_update = old_profile.option.as_ref().and_then(|o| o.allow_auto_update); let new_allow_auto_update = new_option.allow_auto_update; (old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update) } else { false }; - profiles_patch_item_safe(&index, &profile) - .await - .stringify_err()?; + profiles_patch_item_safe(&index, &profile).await.stringify_err()?; // 如果更新间隔或允许自动更新变更,异步刷新定时器 if should_refresh_timer { @@ -498,9 +453,7 @@ pub async fn view_profile(index: String) -> CmdResult { .as_ref() .ok_or("the file field is null")?; - let path = dirs::app_profiles_dir() - .stringify_err()? - .join(file.as_str()); + let path = dirs::app_profiles_dir().stringify_err()?.join(file.as_str()); if !path.exists() { ret_err!("the file not found"); } @@ -515,11 +468,7 @@ pub async fn read_profile_file(index: String) -> CmdResult { let profiles = Config::profiles().await; let profiles_ref = profiles.latest_arc(); PrfItem { - file: profiles_ref - .get_item(&index) - .stringify_err()? - .file - .to_owned(), + file: profiles_ref.get_item(&index).stringify_err()?.file.to_owned(), ..Default::default() } }; diff --git a/clash-verge-rev/src-tauri/src/cmd/runtime.rs b/clash-verge-rev/src-tauri/src/cmd/runtime.rs index 24f0cbec5c..2da860bd03 100644 --- a/clash-verge-rev/src-tauri/src/cmd/runtime.rs +++ b/clash-verge-rev/src-tauri/src/cmd/runtime.rs @@ -58,8 +58,7 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm while let Some(proxy) = proxies.iter().find(|proxy| { if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy { - proxy_map.get("name").map(|x| x.as_str()) == proxy_name - && proxy_map.get("dialer-proxy").is_some() + proxy_map.get("name").map(|x| x.as_str()) == proxy_name && proxy_map.get("dialer-proxy").is_some() } else { false } @@ -94,15 +93,13 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm /// 更新运行时链式代理配置 #[tauri::command] -pub async fn update_proxy_chain_config_in_runtime( - proxy_chain_config: Option, -) -> CmdResult<()> { +pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option) -> CmdResult<()> { { let runtime = Config::runtime().await; runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config)); // 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply() } - logging_error!(Type::Core, CoreManager::global().update_config().await); + logging_error!(Type::Core, CoreManager::global().apply_generate_confihg().await); Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/save_profile.rs b/clash-verge-rev/src-tauri/src/cmd/save_profile.rs index 9ccbce15aa..39de7a09b9 100644 --- a/clash-verge-rev/src-tauri/src/cmd/save_profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/save_profile.rs @@ -71,10 +71,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR Ok(()) } -async fn restore_original( - file_path: &std::path::Path, - original_content: &str, -) -> Result<(), String> { +async fn restore_original(file_path: &std::path::Path, original_content: &str) -> Result<(), String> { fs::write(file_path, original_content).await.stringify_err() } @@ -90,34 +87,20 @@ async fn handle_merge_file( file_path: &std::path::Path, original_content: &str, ) -> CmdResult { - logging!( - info, - Type::Config, - "[cmd配置save] 检测到merge文件,只进行语法验证" - ); + logging!(info, Type::Config, "[cmd配置save] 检测到merge文件,只进行语法验证"); match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await { Ok((true, _)) => { logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过"); if let Err(e) = CoreManager::global().update_config().await { - logging!( - warn, - Type::Config, - "[cmd配置save] 更新整体配置时发生错误: {}", - e - ); + logging!(warn, Type::Config, "[cmd配置save] 更新整体配置时发生错误: {}", e); } else { handle::Handle::refresh_clash(); } Ok(true) } Ok((false, error_msg)) => { - logging!( - warn, - Type::Config, - "[cmd配置save] merge文件语法验证失败: {}", - error_msg - ); + logging!(warn, Type::Config, "[cmd配置save] merge文件语法验证失败: {}", error_msg); restore_original(file_path, original_content).await?; let result = (false, error_msg.clone()); crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); @@ -149,27 +132,15 @@ async fn handle_full_validation( || error_msg.contains("Failed to read file:") || (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str)) { - logging!( - info, - Type::Config, - "[cmd配置save] YAML配置文件验证失败,发送通知" - ); + logging!(info, Type::Config, "[cmd配置save] YAML配置文件验证失败,发送通知"); let result = (false, error_msg.to_owned()); crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件"); } else if is_script_error(&error_msg, file_path_str) { - logging!( - info, - Type::Config, - "[cmd配置save] 脚本文件验证失败,发送通知" - ); + logging!(info, Type::Config, "[cmd配置save] 脚本文件验证失败,发送通知"); let result = (false, error_msg.to_owned()); crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); } else { - logging!( - info, - Type::Config, - "[cmd配置save] 其他类型验证失败,发送一般通知" - ); + logging!(info, Type::Config, "[cmd配置save] 其他类型验证失败,发送一般通知"); handle::Handle::notice_message("config_validate::error", error_msg.to_owned()); } diff --git a/clash-verge-rev/src-tauri/src/cmd/service.rs b/clash-verge-rev/src-tauri/src/cmd/service.rs index 3f7b922738..0b475f99dc 100644 --- a/clash-verge-rev/src-tauri/src/cmd/service.rs +++ b/clash-verge-rev/src-tauri/src/cmd/service.rs @@ -3,12 +3,7 @@ use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus}; use smartstring::SmartString; async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult { - if let Err(e) = SERVICE_MANAGER - .lock() - .await - .handle_service_status(&status) - .await - { + if let Err(e) = SERVICE_MANAGER.lock().await.handle_service_status(&status).await { let emsg = format!("{} Service failed: {}", op_type, e); return Err(SmartString::from(emsg)); } diff --git a/clash-verge-rev/src-tauri/src/cmd/validate.rs b/clash-verge-rev/src-tauri/src/cmd/validate.rs index ee80675be8..52e4af159a 100644 --- a/clash-verge-rev/src-tauri/src/cmd/validate.rs +++ b/clash-verge-rev/src-tauri/src/cmd/validate.rs @@ -47,12 +47,7 @@ pub async fn validate_script_file(file_path: String) -> CmdResult { } Err(e) => { let error_msg = e.to_string(); - logging!( - error, - Type::Config, - "验证脚本文件过程发生错误: {}", - error_msg - ); + logging!(error, Type::Config, "验证脚本文件过程发生错误: {}", error_msg); handle::Handle::notice_message("config_validate::process_terminated", &error_msg); Ok(false) } @@ -64,13 +59,7 @@ pub async fn validate_script_file(file_path: String) -> CmdResult { pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) { if !result.0 { let error_msg = &result.1; - logging!( - info, - Type::Config, - "[通知] 处理{}验证错误: {}", - file_type, - error_msg - ); + logging!(info, Type::Config, "[通知] 处理{}验证错误: {}", file_type, error_msg); // 检查是否为merge文件 let is_merge_file = file_type.contains("合并"); diff --git a/clash-verge-rev/src-tauri/src/cmd/webdav.rs b/clash-verge-rev/src-tauri/src/cmd/webdav.rs index 30e2465409..5f9f00fcca 100644 --- a/clash-verge-rev/src-tauri/src/cmd/webdav.rs +++ b/clash-verge-rev/src-tauri/src/cmd/webdav.rs @@ -28,9 +28,7 @@ pub async fn save_webdav_config(url: String, username: String, password: String) /// 创建 WebDAV 备份并上传 #[tauri::command] pub async fn create_webdav_backup() -> CmdResult<()> { - feat::create_backup_and_upload_webdav() - .await - .stringify_err() + feat::create_backup_and_upload_webdav().await.stringify_err() } /// 列出 WebDAV 上的备份文件 diff --git a/clash-verge-rev/src-tauri/src/config/clash.rs b/clash-verge-rev/src-tauri/src/config/clash.rs index f036c9ce61..1d2f53b99f 100644 --- a/clash-verge-rev/src-tauri/src/config/clash.rs +++ b/clash-verge-rev/src-tauri/src/config/clash.rs @@ -133,15 +133,9 @@ impl IClashTemp { config.insert("external-controller".into(), ctrl.into()); #[cfg(unix)] - config.insert( - "external-controller-unix".into(), - external_controller_unix.into(), - ); + config.insert("external-controller-unix".into(), external_controller_unix.into()); #[cfg(windows)] - config.insert( - "external-controller-pipe".into(), - external_controller_pipe.into(), - ); + config.insert("external-controller-pipe".into(), external_controller_pipe.into()); config } @@ -152,12 +146,7 @@ impl IClashTemp { } pub async fn save_config(&self) -> Result<()> { - help::save_yaml( - &dirs::clash_path()?, - &self.0, - Some("# Generated by Clash Verge"), - ) - .await + help::save_yaml(&dirs::clash_path()?, &self.0, Some("# Generated by Clash Verge")).await } pub fn get_mixed_port(&self) -> u16 { @@ -282,9 +271,7 @@ impl IClashTemp { false => val_str.to_owned(), }; - SocketAddr::from_str(val.as_str()) - .ok() - .map(|s| s.to_string()) + SocketAddr::from_str(val.as_str()).ok().map(|s| s.to_string()) } None => None, }) @@ -378,40 +365,19 @@ fn test_clash_info() { assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9097")); - assert_eq!( - get_case(8888, "127.0.0.1:8888"), - get_result(8888, "127.0.0.1:8888") - ); + assert_eq!(get_case(8888, "127.0.0.1:8888"), get_result(8888, "127.0.0.1:8888")); - assert_eq!( - get_case(8888, " :98888 "), - get_result(8888, "127.0.0.1:9097") - ); + assert_eq!(get_case(8888, " :98888 "), get_result(8888, "127.0.0.1:9097")); - assert_eq!( - get_case(8888, "0.0.0.0:8080 "), - get_result(8888, "127.0.0.1:8080") - ); + assert_eq!(get_case(8888, "0.0.0.0:8080 "), get_result(8888, "127.0.0.1:8080")); - assert_eq!( - get_case(8888, "0.0.0.0:8080"), - get_result(8888, "127.0.0.1:8080") - ); + assert_eq!(get_case(8888, "0.0.0.0:8080"), get_result(8888, "127.0.0.1:8080")); - assert_eq!( - get_case(8888, "[::]:8080"), - get_result(8888, "127.0.0.1:8080") - ); + assert_eq!(get_case(8888, "[::]:8080"), get_result(8888, "127.0.0.1:8080")); - assert_eq!( - get_case(8888, "192.168.1.1:8080"), - get_result(8888, "192.168.1.1:8080") - ); + assert_eq!(get_case(8888, "192.168.1.1:8080"), get_result(8888, "192.168.1.1:8080")); - assert_eq!( - get_case(8888, "192.168.1.1:80800"), - get_result(8888, "127.0.0.1:9097") - ); + assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097")); } #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] diff --git a/clash-verge-rev/src-tauri/src/config/config.rs b/clash-verge-rev/src-tauri/src/config/config.rs index 4c391de62d..99f270e2c1 100644 --- a/clash-verge-rev/src-tauri/src/config/config.rs +++ b/clash-verge-rev/src-tauri/src/config/config.rs @@ -252,8 +252,7 @@ mod tests { #[allow(unused_variables)] #[allow(clippy::expect_used)] fn test_prfitem_from_merge_size() { - let merge_item = - PrfItem::from_merge(Some("Merge".into())).expect("Failed to create merge item in test"); + let merge_item = PrfItem::from_merge(Some("Merge".into())).expect("Failed to create merge item in test"); let prfitem_size = mem::size_of_val(&merge_item); // Boxed version let boxed_merge_item = Box::new(merge_item); @@ -276,9 +275,6 @@ mod tests { fn test_draft_size_boxed() { let draft = Draft::new(Box::new(IRuntime::new())); let box_iruntime_size = std::mem::size_of_val(&draft); - assert_eq!( - box_iruntime_size, - std::mem::size_of::>>() - ); + assert_eq!(box_iruntime_size, std::mem::size_of::>>()); } } diff --git a/clash-verge-rev/src-tauri/src/config/encrypt.rs b/clash-verge-rev/src-tauri/src/config/encrypt.rs index 71743fae42..0a82ba70d8 100644 --- a/clash-verge-rev/src-tauri/src/config/encrypt.rs +++ b/clash-verge-rev/src-tauri/src/config/encrypt.rs @@ -86,8 +86,7 @@ where match encrypted_opt { Some(encrypted) if !encrypted.is_empty() => { - let decrypted_string = - decrypt_data(&encrypted).map_err(serde::de::Error::custom)?; + let decrypted_string = decrypt_data(&encrypted).map_err(serde::de::Error::custom)?; serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom) } _ => Ok(T::default()), diff --git a/clash-verge-rev/src-tauri/src/config/prfitem.rs b/clash-verge-rev/src-tauri/src/config/prfitem.rs index 3e21dd583c..24dc7eb77b 100644 --- a/clash-verge-rev/src-tauri/src/config/prfitem.rs +++ b/clash-verge-rev/src-tauri/src/config/prfitem.rs @@ -129,9 +129,8 @@ impl PrfOption { result.user_agent = b_ref.user_agent.clone().or(result.user_agent); result.with_proxy = b_ref.with_proxy.or(result.with_proxy); result.self_proxy = b_ref.self_proxy.or(result.self_proxy); - result.danger_accept_invalid_certs = b_ref - .danger_accept_invalid_certs - .or(result.danger_accept_invalid_certs); + result.danger_accept_invalid_certs = + b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs); result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); result.update_interval = b_ref.update_interval.or(result.update_interval); result.merge = b_ref.merge.clone().or(result.merge); @@ -259,8 +258,7 @@ impl PrfItem { ) -> Result { let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false)); let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false)); - let accept_invalid_certs = - option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false)); + let accept_invalid_certs = option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false)); let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true)); let user_agent = option.and_then(|o| o.user_agent.clone()); let update_interval = option.and_then(|o| o.update_interval); @@ -282,13 +280,7 @@ impl PrfItem { // 使用网络管理器发送请求 let resp = match NetworkManager::new() - .get_with_interrupt( - url, - proxy_type, - Some(timeout), - user_agent.clone(), - accept_invalid_certs, - ) + .get_with_interrupt(url, proxy_type, Some(timeout), user_agent.clone(), accept_invalid_certs) .await { Ok(r) => r, @@ -348,10 +340,7 @@ impl PrfItem { }, } } - None => Some( - crate::utils::help::get_last_part_and_decode(url) - .unwrap_or_else(|| "Remote File".into()), - ), + None => Some(crate::utils::help::get_last_part_and_decode(url).unwrap_or_else(|| "Remote File".into())), }; let update_interval = match update_interval { Some(val) => Some(val), @@ -374,19 +363,16 @@ impl PrfItem { let uid = help::get_uid("R").into(); let file = format!("{uid}.yaml").into(); - let name = name.map(|s| s.to_owned()).unwrap_or_else(|| { - filename - .map(|s| s.into()) - .unwrap_or_else(|| "Remote File".into()) - }); + let name = name + .map(|s| s.to_owned()) + .unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into())); let data = resp.text_with_charset()?; // process the charset "UTF-8 with BOM" let data = data.trim_start_matches('\u{feff}'); // check the data whether the valid yaml format - let yaml = serde_yaml_ng::from_str::(data) - .context("the remote profile data is invalid yaml")?; + let yaml = serde_yaml_ng::from_str::(data).context("the remote profile data is invalid yaml")?; if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") { bail!("profile does not contain `proxies` or `proxy-providers`"); @@ -534,9 +520,7 @@ impl PrfItem { .as_ref() .ok_or_else(|| anyhow::anyhow!("could not find the file"))?; let path = dirs::app_profiles_dir()?.join(file.as_str()); - let content = fs::read_to_string(path) - .await - .context("failed to read the file")?; + let content = fs::read_to_string(path).await.context("failed to read the file")?; Ok(content.into()) } diff --git a/clash-verge-rev/src-tauri/src/config/profiles.rs b/clash-verge-rev/src-tauri/src/config/profiles.rs index b70a209688..cb386689f2 100644 --- a/clash-verge-rev/src-tauri/src/config/profiles.rs +++ b/clash-verge-rev/src-tauri/src/config/profiles.rs @@ -45,10 +45,7 @@ macro_rules! patch { impl IProfiles { // Helper to find and remove an item by uid from the items vec, returning its file name (if any). - fn take_item_file_by_uid( - items: &mut Vec, - target_uid: Option, - ) -> Option { + fn take_item_file_by_uid(items: &mut Vec, target_uid: Option) -> Option { for (i, _) in items.iter().enumerate() { if items[i].uid == target_uid { return items.remove(i).file; @@ -84,12 +81,7 @@ impl IProfiles { } pub async fn save_file(&self) -> Result<()> { - help::save_yaml( - &dirs::profiles_path()?, - self, - Some("# Profiles Config for Clash Verge"), - ) - .await + help::save_yaml(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await } /// 只修改current,valid和chain @@ -159,9 +151,10 @@ impl IProfiles { bail!("the file should not be null"); } - let file = item.file.clone().ok_or_else(|| { - anyhow::anyhow!("file field is required when file_data is provided") - })?; + let file = item + .file + .clone() + .ok_or_else(|| anyhow::anyhow!("file field is required when file_data is provided"))?; let path = dirs::app_profiles_dir()?.join(file.as_str()); fs::write(&path, file_data.as_bytes()) @@ -169,9 +162,7 @@ impl IProfiles { .with_context(|| format!("failed to write to file \"{file}\""))?; } - if self.current.is_none() - && (item.itype == Some("remote".into()) || item.itype == Some("local".into())) - { + if self.current.is_none() && (item.itype == Some("remote".into()) || item.itype == Some("local".into())) { self.current = uid.to_owned(); } @@ -259,11 +250,8 @@ impl IProfiles { // move the field value after save if let Some(file_data) = item.file_data.take() { let file = each.file.take(); - let file = file.unwrap_or_else(|| { - item.file - .take() - .unwrap_or_else(|| format!("{}.yaml", &uid).into()) - }); + let file = + file.unwrap_or_else(|| item.file.take().unwrap_or_else(|| format!("{}.yaml", &uid).into())); // the file must exists each.file = Some(file.clone()); @@ -298,42 +286,24 @@ impl IProfiles { // remove the main item (if exists) and delete its file if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) { - let _ = dirs::app_profiles_dir()? - .join(file.as_str()) - .remove_if_exists() - .await; + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; } // remove related extension items (merge, script, rules, proxies, groups) if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) { - let _ = dirs::app_profiles_dir()? - .join(file.as_str()) - .remove_if_exists() - .await; + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; } if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) { - let _ = dirs::app_profiles_dir()? - .join(file.as_str()) - .remove_if_exists() - .await; + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; } if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) { - let _ = dirs::app_profiles_dir()? - .join(file.as_str()) - .remove_if_exists() - .await; + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; } if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) { - let _ = dirs::app_profiles_dir()? - .join(file.as_str()) - .remove_if_exists() - .await; + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; } if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) { - let _ = dirs::app_profiles_dir()? - .join(file.as_str()) - .remove_if_exists() - .await; + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; } // delete the original uid if current == *uid { @@ -381,11 +351,7 @@ impl IProfiles { .filter_map(|e| { if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) { let is_current = self.is_current_profile_index(uid); - let preview = IProfilePreview { - uid, - name, - is_current, - }; + let preview = IProfilePreview { uid, name, is_current }; Some(preview) } else { None @@ -458,11 +424,7 @@ impl IProfiles { } Err(e) => { failed_deletions.push(format!("{file_name}: {e}").into()); - logging!( - warn, - Type::Config, - "Warning: 清理文件失败: {file_name} - {e}" - ); + logging!(warn, Type::Config, "Warning: 清理文件失败: {file_name} - {e}"); } } } @@ -584,10 +546,7 @@ impl IProfiles { // 特殊的Send-safe helper函数,完全避免跨await持有guard use crate::config::Config; -pub async fn profiles_append_item_with_filedata_safe( - item: &PrfItem, - file_data: Option, -) -> Result<()> { +pub async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file_data: Option) -> Result<()> { let item = &mut PrfItem::from(item, file_data).await?; profiles_append_item_safe(item).await } diff --git a/clash-verge-rev/src-tauri/src/config/verge.rs b/clash-verge-rev/src-tauri/src/config/verge.rs index f47e4b02b7..ad43af3313 100644 --- a/clash-verge-rev/src-tauri/src/config/verge.rs +++ b/clash-verge-rev/src-tauri/src/config/verge.rs @@ -308,12 +308,7 @@ impl IVerge { Self::reload_config_after_fix(config).await?; } else { - logging!( - info, - Type::Config, - "clash_core配置验证通过: {:?}", - config.clash_core - ); + logging!(info, Type::Config, "clash_core配置验证通过: {:?}", config.clash_core); } Ok(()) @@ -338,15 +333,11 @@ impl IVerge { } pub fn get_valid_clash_core(&self) -> String { - self.clash_core - .clone() - .unwrap_or_else(|| "verge-mihomo".into()) + self.clash_core.clone().unwrap_or_else(|| "verge-mihomo".into()) } fn get_system_language() -> String { - let sys_lang = sys_locale::get_locale() - .unwrap_or_else(|| "en".into()) - .to_lowercase(); + let sys_lang = sys_locale::get_locale().unwrap_or_else(|| "en".into()).to_lowercase(); let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en"); let supported_languages = i18n::get_supported_languages(); diff --git a/clash-verge-rev/src-tauri/src/core/backup.rs b/clash-verge-rev/src-tauri/src/core/backup.rs index 08476d5ccc..2afa311687 100644 --- a/clash-verge-rev/src-tauri/src/core/backup.rs +++ b/clash-verge-rev/src-tauri/src/core/backup.rs @@ -84,10 +84,7 @@ impl WebDavClient { } else { // 释放锁后获取异步配置 let verge = Config::verge().await.data_arc(); - if verge.webdav_url.is_none() - || verge.webdav_username.is_none() - || verge.webdav_password.is_none() - { + if verge.webdav_url.is_none() || verge.webdav_username.is_none() || verge.webdav_password.is_none() { let msg: String = "Unable to create web dav client, please make sure the webdav config is correct".into(); return Err(anyhow::Error::msg(msg)); @@ -129,10 +126,7 @@ impl WebDavClient { .build()?, ) .set_host(config.url.into()) - .set_auth(reqwest_dav::Auth::Basic( - config.username.into(), - config.password.into(), - )) + .set_auth(reqwest_dav::Auth::Basic(config.username.into(), config.password.into())) .build()?; // 尝试检查目录是否存在,如果不存在尝试创建 @@ -144,18 +138,10 @@ impl WebDavClient { match client.mkcol(dirs::BACKUP_DIR).await { Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"), Err(e) => { - logging!( - warn, - Type::Backup, - "Warning: Failed to create backup directory: {}", - e - ); + logging!(warn, Type::Backup, "Warning: Failed to create backup directory: {}", e); // 清除缓存,强制下次重新尝试 self.reset(); - return Err(anyhow::Error::msg(format!( - "Failed to create backup directory: {}", - e - ))); + return Err(anyhow::Error::msg(format!("Failed to create backup directory: {}", e))); } } } @@ -191,11 +177,7 @@ impl WebDavClient { match upload_result { Err(_) => { - logging!( - warn, - Type::Backup, - "Warning: Upload timed out, retrying once" - ); + logging!(warn, Type::Backup, "Warning: Upload timed out, retrying once"); tokio::time::sleep(Duration::from_millis(500)).await; timeout( Duration::from_secs(TIMEOUT_UPLOAD), @@ -206,11 +188,7 @@ impl WebDavClient { } Ok(Err(e)) => { - logging!( - warn, - Type::Backup, - "Warning: Upload failed, retrying once: {e}" - ); + logging!(warn, Type::Backup, "Warning: Upload failed, retrying once: {e}"); tokio::time::sleep(Duration::from_millis(500)).await; timeout( Duration::from_secs(TIMEOUT_UPLOAD), @@ -243,9 +221,7 @@ impl WebDavClient { let path = format!("{}/", dirs::BACKUP_DIR); let fut = async { - let files = client - .list(path.as_str(), reqwest_dav::Depth::Number(1)) - .await?; + let files = client.list(path.as_str(), reqwest_dav::Depth::Number(1)).await?; let mut final_files = Vec::new(); for file in files { if let ListEntity::File(file) = file { diff --git a/clash-verge-rev/src-tauri/src/core/hotkey.rs b/clash-verge-rev/src-tauri/src/core/hotkey.rs index fb72551f75..be7b927b52 100755 --- a/clash-verge-rev/src-tauri/src/core/hotkey.rs +++ b/clash-verge-rev/src-tauri/src/core/hotkey.rs @@ -177,10 +177,7 @@ impl Hotkey { "Failed to reactivate subscriptions via hotkey: {}", err ); - handle::Handle::notice_message( - "reactivate_profiles::error", - err.to_string(), - ); + handle::Handle::notice_message("reactivate_profiles::error", err.to_string()); } }); } @@ -205,8 +202,7 @@ impl Hotkey { pub async fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> { let hotkey_str = hotkey.to_string(); let function = hotkey.function(); - self.register_hotkey_with_function(&hotkey_str, function) - .await + self.register_hotkey_with_function(&hotkey_str, function).await } #[cfg(target_os = "macos")] @@ -218,11 +214,7 @@ impl Hotkey { /// Register a hotkey with function enum #[allow(clippy::unused_async)] - pub async fn register_hotkey_with_function( - &self, - hotkey: &str, - function: HotkeyFunction, - ) -> Result<()> { + pub async fn register_hotkey_with_function(&self, hotkey: &str, function: HotkeyFunction) -> Result<()> { let app_handle = handle::Handle::app_handle(); let manager = app_handle.global_shortcut(); @@ -261,11 +253,8 @@ impl Hotkey { AsyncHandler::spawn(move || async move { logging!(debug, Type::Hotkey, "Executing function directly"); - let is_enable_global_hotkey = Config::verge() - .await - .data_arc() - .enable_global_hotkey - .unwrap_or(true); + let is_enable_global_hotkey = + Config::verge().await.data_arc().enable_global_hotkey.unwrap_or(true); if is_enable_global_hotkey { Self::execute_function(function); @@ -298,8 +287,12 @@ singleton!(Hotkey, INSTANCE); impl Hotkey { pub async fn init(&self, skip: bool) -> Result<()> { + if skip { + logging!(debug, Type::Hotkey, "skip register all hotkeys"); + return Ok(()); + } let verge = Config::verge().await; - let enable_global_hotkey = !skip && verge.data_arc().enable_global_hotkey.unwrap_or(true); + let enable_global_hotkey = verge.latest_arc().enable_global_hotkey.unwrap_or(true); logging!( debug, @@ -309,15 +302,10 @@ impl Hotkey { ); // Extract hotkeys data before async operations - let hotkeys = verge.data_arc().hotkeys.clone(); + let hotkeys = verge.latest_arc().hotkeys.clone(); if let Some(hotkeys) = hotkeys { - logging!( - debug, - Type::Hotkey, - "Has {} hotkeys need to register", - hotkeys.len() - ); + logging!(debug, Type::Hotkey, "Has {} hotkeys need to register", hotkeys.len()); for hotkey in hotkeys.iter() { let mut iter = hotkey.split(','); @@ -326,13 +314,7 @@ impl Hotkey { match (key, func) { (Some(key), Some(func)) => { - logging!( - debug, - Type::Hotkey, - "Registering hotkey: {} -> {}", - key, - func - ); + logging!(debug, Type::Hotkey, "Registering hotkey: {} -> {}", key, func); if let Err(e) = self.register(key, func).await { logging!( error, @@ -465,12 +447,7 @@ impl Drop for Hotkey { fn drop(&mut self) { let app_handle = handle::Handle::app_handle(); if let Err(e) = app_handle.global_shortcut().unregister_all() { - logging!( - error, - Type::Hotkey, - "Error unregistering all hotkeys: {:?}", - e - ); + logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e); } } } diff --git a/clash-verge-rev/src-tauri/src/core/logger.rs b/clash-verge-rev/src-tauri/src/core/logger.rs index 8fd38c3d1f..ed94d96aff 100644 --- a/clash-verge-rev/src-tauri/src/core/logger.rs +++ b/clash-verge-rev/src-tauri/src/core/logger.rs @@ -1,6 +1 @@ -use std::sync::Arc; - -use clash_verge_logger::AsyncLogger; -use once_cell::sync::Lazy; - -pub static CLASH_LOGGER: Lazy> = Lazy::new(|| Arc::new(AsyncLogger::new())); +// TODO: global logger to record verge log message diff --git a/clash-verge-rev/src-tauri/src/core/manager/config.rs b/clash-verge-rev/src-tauri/src/core/manager/config.rs index b0a4992877..8ef82d7a2f 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/config.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/config.rs @@ -60,7 +60,10 @@ impl CoreManager { async fn perform_config_update(&self) -> Result<(bool, String)> { Config::generate().await?; + self.apply_generate_confihg().await + } + pub async fn apply_generate_confihg(&self) -> Result<(bool, String)> { match CoreConfigValidator::global().validate_config().await { Ok((true, _)) => { let run_path = Config::generate_file(ConfigType::Run).await?; @@ -94,9 +97,6 @@ impl CoreManager { } async fn reload_config(&self, path: &str) -> Result<(), MihomoError> { - handle::Handle::mihomo() - .await - .reload_config(true, path) - .await + handle::Handle::mihomo().await.reload_config(true, path).await } } diff --git a/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs b/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs index 89926415cc..0298773a82 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs @@ -2,10 +2,8 @@ use super::{CoreManager, RunningMode}; use crate::cmd::StringifyErr as _; use crate::config::{Config, IVerge}; use crate::core::handle::Handle; -use crate::core::{ - logger::CLASH_LOGGER, - service::{SERVICE_MANAGER, ServiceStatus}, -}; +use crate::core::manager::CLASH_LOGGER; +use crate::core::service::{SERVICE_MANAGER, ServiceStatus}; use anyhow::Result; use clash_verge_logging::{Type, logging}; use scopeguard::defer; @@ -44,11 +42,6 @@ impl CoreManager { pub async fn restart_core(&self) -> Result<()> { logging!(info, Type::Core, "Restarting core"); self.stop_core().await?; - - if SERVICE_MANAGER.lock().await.init().await.is_ok() { - let _ = SERVICE_MANAGER.lock().await.refresh().await; - } - self.start_core().await } @@ -85,10 +78,7 @@ impl CoreManager { fn after_core_process(&self) { let app_handle = Handle::app_handle(); - tauri_plugin_clash_verge_sysinfo::set_app_core_mode( - app_handle, - self.get_running_mode().to_string(), - ); + tauri_plugin_clash_verge_sysinfo::set_app_core_mode(app_handle, self.get_running_mode().to_string()); } #[cfg(target_os = "windows")] @@ -96,11 +86,7 @@ impl CoreManager { use crate::{config::Config, constants::timing}; use backoff::{Error as BackoffError, ExponentialBackoff}; - let needs_service = Config::verge() - .await - .latest_arc() - .enable_tun_mode - .unwrap_or(false); + let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false); if !needs_service { return; @@ -128,9 +114,7 @@ impl CoreManager { if matches!(manager.current(), ServiceStatus::Ready) { Ok(()) } else { - Err(BackoffError::transient(anyhow::anyhow!( - "Service not ready" - ))) + Err(BackoffError::transient(anyhow::anyhow!("Service not ready"))) } }; diff --git a/clash-verge-rev/src-tauri/src/core/manager/mod.rs b/clash-verge-rev/src-tauri/src/core/manager/mod.rs index ed0f1e53d2..93dc0bbd8a 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/mod.rs @@ -4,11 +4,15 @@ mod state; use anyhow::Result; use arc_swap::{ArcSwap, ArcSwapOption}; +use clash_verge_logger::AsyncLogger; +use once_cell::sync::Lazy; use std::{fmt, sync::Arc, time::Instant}; use tauri_plugin_shell::process::CommandChild; use crate::singleton; +pub(crate) static CLASH_LOGGER: Lazy> = Lazy::new(|| Arc::new(AsyncLogger::new())); + #[derive(Debug, serde::Serialize, PartialEq, Eq)] pub enum RunningMode { Service, diff --git a/clash-verge-rev/src-tauri/src/core/manager/state.rs b/clash-verge-rev/src-tauri/src/core/manager/state.rs index 897383c254..e2c9e23025 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/state.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/state.rs @@ -1,8 +1,8 @@ use super::{CoreManager, RunningMode}; use crate::{ AsyncHandler, - config::Config, - core::{handle, logger::CLASH_LOGGER, service}, + config::{Config, IClashTemp}, + core::{handle, manager::CLASH_LOGGER, service}, logging, utils::{dirs, init::sidecar_writer}, }; @@ -39,6 +39,12 @@ impl CoreManager { dirs::path_to_str(&config_dir)?, "-f", dirs::path_to_str(&config_file)?, + if cfg!(windows) { + "-ext-ctl-pipe" + } else { + "-ext-ctl-unix" + }, + &IClashTemp::guard_external_controller_ipc(), ]) .spawn()?; @@ -48,8 +54,7 @@ impl CoreManager { self.set_running_child_sidecar(child); self.set_running_mode(RunningMode::Sidecar); - let shared_writer: SharedWriter = - std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?)); + let shared_writer: SharedWriter = std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?)); AsyncHandler::spawn(|| async move { while let Some(event) = rx.recv().await { @@ -58,12 +63,7 @@ impl CoreManager { | tauri_plugin_shell::process::CommandEvent::Stderr(line) => { let mut now = DeferredNow::default(); let message = CompactString::from(String::from_utf8_lossy(&line).as_ref()); - write_sidecar_log( - shared_writer.lock().await, - &mut now, - Level::Error, - &message, - ); + write_sidecar_log(shared_writer.lock().await, &mut now, Level::Error, &message); CLASH_LOGGER.append_log(message).await; } tauri_plugin_shell::process::CommandEvent::Terminated(term) => { @@ -75,12 +75,7 @@ impl CoreManager { } else { CompactString::from("Process terminated") }; - write_sidecar_log( - shared_writer.lock().await, - &mut now, - Level::Info, - &message, - ); + write_sidecar_log(shared_writer.lock().await, &mut now, Level::Info, &message); CLASH_LOGGER.clear_logs().await; break; } diff --git a/clash-verge-rev/src-tauri/src/core/notification.rs b/clash-verge-rev/src-tauri/src/core/notification.rs index 472854a4c4..aa8246ff12 100644 --- a/clash-verge-rev/src-tauri/src/core/notification.rs +++ b/clash-verge-rev/src-tauri/src/core/notification.rs @@ -9,7 +9,7 @@ use std::{ mpsc, }, thread, - time::{Duration, Instant}, + time::Instant, }; use tauri::{Emitter as _, WebviewWindow}; @@ -80,25 +80,22 @@ impl NotificationSystem { match result { Ok(handle) => self.worker_handle = Some(handle), - Err(e) => logging!( - error, - Type::System, - "Failed to start notification worker: {}", - e - ), + Err(e) => logging!(error, Type::System, "Failed to start notification worker: {}", e), } } fn worker_loop(rx: mpsc::Receiver) { + let handle = Handle::global(); loop { - let handle = Handle::global(); if handle.is_exiting() { break; } - match rx.recv_timeout(Duration::from_millis(1_000)) { + match rx.recv() { Ok(event) => Self::process_event(handle, event), - Err(mpsc::RecvTimeoutError::Timeout) => (), - Err(mpsc::RecvTimeoutError::Disconnected) => break, + Err(e) => { + logging!(error, Type::System, "Notification System will exit, recv error: {}", e); + break; + } } } } @@ -148,31 +145,19 @@ impl NotificationSystem { } } - fn serialize_event( - &self, - event: FrontendEvent, - ) -> (&'static str, Result) { + fn serialize_event(&self, event: FrontendEvent) -> (&'static str, Result) { use serde_json::json; match event { FrontendEvent::RefreshClash => ("verge://refresh-clash-config", Ok(json!("yes"))), FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))), - FrontendEvent::NoticeMessage { status, message } => ( - "verge://notice-message", - serde_json::to_value((status, message)), - ), - FrontendEvent::ProfileChanged { current_profile_id } => { - ("profile-changed", Ok(json!(current_profile_id))) - } - FrontendEvent::TimerUpdated { profile_index } => { - ("verge://timer-updated", Ok(json!(profile_index))) - } - FrontendEvent::ProfileUpdateStarted { uid } => { - ("profile-update-started", Ok(json!({ "uid": uid }))) - } - FrontendEvent::ProfileUpdateCompleted { uid } => { - ("profile-update-completed", Ok(json!({ "uid": uid }))) + FrontendEvent::NoticeMessage { status, message } => { + ("verge://notice-message", serde_json::to_value((status, message))) } + FrontendEvent::ProfileChanged { current_profile_id } => ("profile-changed", Ok(json!(current_profile_id))), + FrontendEvent::TimerUpdated { profile_index } => ("verge://timer-updated", Ok(json!(profile_index))), + FrontendEvent::ProfileUpdateStarted { uid } => ("profile-update-started", Ok(json!({ "uid": uid }))), + FrontendEvent::ProfileUpdateCompleted { uid } => ("profile-update-completed", Ok(json!({ "uid": uid }))), } } @@ -182,12 +167,7 @@ impl NotificationSystem { let errors = self.stats.total_errors.load(Ordering::Relaxed); if errors > retry::EVENT_EMIT_THRESHOLD && !self.emergency_mode.load(Ordering::Acquire) { - logging!( - warn, - Type::Frontend, - "Entering emergency mode after {} errors", - errors - ); + logging!(warn, Type::Frontend, "Entering emergency mode after {} errors", errors); self.emergency_mode.store(true, Ordering::Release); } } diff --git a/clash-verge-rev/src-tauri/src/core/service.rs b/clash-verge-rev/src-tauri/src/core/service.rs index 34c2633264..1974473d63 100644 --- a/clash-verge-rev/src-tauri/src/core/service.rs +++ b/clash-verge-rev/src-tauri/src/core/service.rs @@ -1,6 +1,6 @@ use crate::{ - config::Config, - core::tray, + config::{Config, IClashTemp}, + core::tray::Tray, utils::{dirs, init::service_writer_config}, }; use anyhow::{Context as _, Result, bail}; @@ -50,9 +50,7 @@ async fn uninstall_service() -> Result<()> { let level = token.privilege_level()?; let status = match level { PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?, - _ => StdCommand::new(uninstall_path) - .creation_flags(0x08000000) - .status()?, + _ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?, }; if !status.success() { @@ -85,16 +83,11 @@ async fn install_service() -> Result<()> { let level = token.privilege_level()?; let status = match level { PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?, - _ => StdCommand::new(install_path) - .creation_flags(0x08000000) - .status()?, + _ => StdCommand::new(install_path).creation_flags(0x08000000).status()?, }; if !status.success() { - bail!( - "failed to install service with status {}", - status.code().unwrap_or(-1) - ); + bail!("failed to install service with status {}", status.code().unwrap_or(-1)); } Ok(()) @@ -123,8 +116,7 @@ async fn reinstall_service() -> Result<()> { async fn uninstall_service() -> Result<()> { logging!(info, Type::Service, "uninstall service"); - let uninstall_path = - tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"); + let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"); if !uninstall_path.exists() { bail!(format!("uninstaller not found: {uninstall_path:?}")); @@ -181,8 +173,7 @@ async fn uninstall_service() -> Result<()> { async fn install_service() -> Result<()> { logging!(info, Type::Service, "install service"); - let install_path = - tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install"); + let install_path = tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install"); if !install_path.exists() { bail!(format!("installer not found: {install_path:?}")); @@ -225,10 +216,7 @@ async fn install_service() -> Result<()> { ); if !status.success() { - bail!( - "failed to install service with status {}", - status.code().unwrap_or(-1) - ); + bail!("failed to install service with status {}", status.code().unwrap_or(-1)); } Ok(()) @@ -275,16 +263,13 @@ async fn uninstall_service() -> Result<()> { crate::utils::i18n::sync_locale().await; - let prompt = rust_i18n::t!("service.adminPrompt").to_string(); - let command = format!( - r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""# - ); + let prompt = rust_i18n::t!("service.adminUninstallPrompt").to_string(); + let command = + format!(r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#); // logging!(debug, Type::Service, "uninstall command: {}", command); - let status = StdCommand::new("osascript") - .args(vec!["-e", &command]) - .status()?; + let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?; if !status.success() { bail!( @@ -311,22 +296,14 @@ async fn install_service() -> Result<()> { crate::utils::i18n::sync_locale().await; - let prompt = rust_i18n::t!("service.adminPrompt").to_string(); - let command = format!( - r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""# - ); + let prompt = rust_i18n::t!("service.adminInstallPrompt").to_string(); + let command = + format!(r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#); - // logging!(debug, Type::Service, "install command: {}", command); - - let status = StdCommand::new("osascript") - .args(vec!["-e", &command]) - .status()?; + let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?; if !status.success() { - bail!( - "failed to install service with status {}", - status.code().unwrap_or(-1) - ); + bail!("failed to install service with status {}", status.code().unwrap_or(-1)); } Ok(()) @@ -351,7 +328,7 @@ async fn reinstall_service() -> Result<()> { } /// 强制重装服务(UI修复按钮) -pub async fn force_reinstall_service() -> Result<()> { +async fn force_reinstall_service() -> Result<()> { logging!(info, Type::Service, "用户请求强制重装服务"); reinstall_service().await.map_err(|err| { logging!(error, Type::Service, "强制重装服务失败: {}", err); @@ -363,9 +340,26 @@ pub async fn force_reinstall_service() -> Result<()> { async fn check_service_version() -> Result { let version_arc: Result = { logging!(info, Type::Service, "开始检查服务版本 (IPC)"); - let response = clash_verge_service_ipc::get_version() - .await - .context("无法连接到Clash Verge Service")?; + let result = clash_verge_service_ipc::get_version().await; + logging!(debug, Type::Service, "检查服务版本 (IPC) 结果: {:?}", result); + + // 检查错误信息是否是JSON序列化错误或预期值错误,以适配老版本服务 + // 这可能是因为老版本服务的API不兼容,导致无法正确解析响应 + // 如果是这种情况,直接返回空字符串,表示无法获取版本 + if let Err(e) = result.as_ref() + && (e.to_string().contains("JSON serialization error") || e.to_string().contains("expected value")) + { + logging!( + warn, + Type::Service, + "服务版本检查失败,可能是老版本服务 API 不兼容: {}", + e + ); + return Ok("".to_string()); + } + + // 因为上面的错误处理 Error 可能会被忽略,所以这里需要再次检查 + let response = result.context("无法连接到Clash Verge Service")?; if response.code > 0 { let err_msg = response.message; logging!(error, Type::Service, "获取服务版本失败: {}", err_msg); @@ -383,10 +377,10 @@ async fn check_service_version() -> Result { } /// 检查服务是否需要重装 -pub async fn check_service_needs_reinstall() -> bool { +pub async fn check_service_needs_reinstall() -> Result { match check_service_version().await { - Ok(version) => version != clash_verge_service_ipc::VERSION, - Err(_) => false, + Ok(version) => Ok(version != clash_verge_service_ipc::VERSION), + Err(e) => Err(e), } } @@ -405,6 +399,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result core_config: CoreConfig { config_path: dirs::path_to_str(config_file)?.into(), core_path: dirs::path_to_str(&bin_path)?.into(), + core_ipc_path: IClashTemp::guard_external_controller_ipc(), config_dir: dirs::path_to_str(&dirs::app_home_dir()?)?.into(), }, log_config: service_writer_config().await?, @@ -428,9 +423,10 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { logging!(info, Type::Service, "正在尝试通过服务启动核心"); - if check_service_needs_reinstall().await { - reinstall_service().await?; - } + let mut manager = SERVICE_MANAGER.lock().await; + let status = manager.check_service_comprehensive().await; + manager.handle_service_status(&status).await?; + drop(manager); logging!(info, Type::Service, "服务已运行且版本匹配,直接使用"); start_with_existing_service(config_file).await @@ -445,12 +441,7 @@ pub(super) async fn get_clash_logs_by_service() -> Result> { if response.code > 0 { let err_msg = response.message; - logging!( - error, - Type::Service, - "获取服务模式下的 Clash 日志失败: {}", - err_msg - ); + logging!(error, Type::Service, "获取服务模式下的 Clash 日志失败: {}", err_msg); bail!(err_msg); } @@ -478,10 +469,23 @@ pub(super) async fn stop_core_by_service() -> Result<()> { /// 检查服务是否正在运行 pub async fn is_service_available() -> Result<()> { + if let Err(e) = Path::metadata(clash_verge_service_ipc::IPC_PATH.as_ref()) { + logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e); + return Err(e.into()); + } clash_verge_service_ipc::connect().await?; Ok(()) } +/// 等待一会,再检查服务是否正在运行 +/// TODO 使用 tokio select 之类机制并结合 timeout 实现更优雅的等待机制,期望等待文件出现,再尝试连接 +pub async fn wait_and_check_service_available(status: &mut ServiceManager) -> Result<()> { + status.0 = ServiceStatus::Unavailable("Waiting for service to be available".into()); + clash_verge_service_ipc::connect().await?; + status.0 = ServiceStatus::Ready; + Ok(()) +} + pub fn is_service_ipc_path_exists() -> bool { Path::new(clash_verge_service_ipc::IPC_PATH).exists() } @@ -493,8 +497,8 @@ impl ServiceManager { pub const fn config() -> clash_verge_service_ipc::IpcConfig { clash_verge_service_ipc::IpcConfig { - default_timeout: Duration::from_millis(30), - retry_delay: Duration::from_millis(250), + default_timeout: Duration::from_millis(100), + retry_delay: Duration::from_millis(200), max_retries: 6, } } @@ -513,18 +517,18 @@ impl ServiceManager { pub async fn refresh(&mut self) -> Result<()> { let status = self.check_service_comprehensive().await; + self.0 = status.clone(); logging_error!(Type::Service, self.handle_service_status(&status).await); - self.0 = status; Ok(()) } /// 综合服务状态检查(一次性完成所有检查) pub async fn check_service_comprehensive(&self) -> ServiceStatus { - match is_service_available().await { - Ok(_) => { - logging!(info, Type::Service, "服务当前可用,检查是否需要重装"); - if check_service_needs_reinstall().await { - logging!(info, Type::Service, "服务需要重装且允许重装"); + match check_service_needs_reinstall().await { + Ok(need) => { + logging!(debug, Type::Service, "服务当前可用,检查是否需要重装"); + if need { + logging!(debug, Type::Service, "服务需要重装且需要重装"); ServiceStatus::NeedsReinstall } else { ServiceStatus::Ready @@ -542,28 +546,22 @@ impl ServiceManager { match status { ServiceStatus::Ready => { logging!(info, Type::Service, "服务就绪,直接启动"); + self.0 = ServiceStatus::Ready; } ServiceStatus::NeedsReinstall | ServiceStatus::ReinstallRequired => { logging!(info, Type::Service, "服务需要重装,执行重装流程"); reinstall_service().await?; - self.0 = ServiceStatus::Ready; + wait_and_check_service_available(self).await?; } ServiceStatus::ForceReinstallRequired => { logging!(info, Type::Service, "服务需要强制重装,执行强制重装流程"); force_reinstall_service().await?; - self.0 = ServiceStatus::Ready; + wait_and_check_service_available(self).await?; } ServiceStatus::InstallRequired => { logging!(info, Type::Service, "需要安装服务,执行安装流程"); install_service().await?; - // compatible with older service version, force reinstall if service is unavailable - // wait for service server is running - tokio::time::sleep(Duration::from_millis(500)).await; - if is_service_available().await.is_err() { - logging!(info, Type::Service, "服务需要强制重装,执行强制重装流程"); - force_reinstall_service().await?; - } - self.0 = ServiceStatus::Ready; + wait_and_check_service_available(self).await?; } ServiceStatus::UninstallRequired => { logging!(info, Type::Service, "服务需要卸载,执行卸载流程"); @@ -571,20 +569,16 @@ impl ServiceManager { self.0 = ServiceStatus::Unavailable("Service Uninstalled".into()); } ServiceStatus::Unavailable(reason) => { - logging!( - info, - Type::Service, - "服务不可用: {},将使用Sidecar模式", - reason - ); + logging!(info, Type::Service, "服务不可用: {},将使用Sidecar模式", reason); self.0 = ServiceStatus::Unavailable(reason.clone()); return Err(anyhow::anyhow!("服务不可用: {}", reason)); } } - let _ = tray::Tray::global().update_menu().await; + + // 防止服务安装成功后,内核未完全启动导致系统托盘无法获取代理节点信息 + Tray::global().update_menu().await?; Ok(()) } } -pub static SERVICE_MANAGER: Lazy> = - Lazy::new(|| Mutex::new(ServiceManager::default())); +pub static SERVICE_MANAGER: Lazy> = Lazy::new(|| Mutex::new(ServiceManager::default())); diff --git a/clash-verge-rev/src-tauri/src/core/sysopt.rs b/clash-verge-rev/src-tauri/src/core/sysopt.rs index 39918369bf..ca1a35200a 100644 --- a/clash-verge-rev/src-tauri/src/core/sysopt.rs +++ b/clash-verge-rev/src-tauri/src/core/sysopt.rs @@ -7,8 +7,6 @@ use crate::{ }; use anyhow::Result; use clash_verge_logging::{Type, logging, logging_error}; -#[cfg(not(target_os = "windows"))] -use parking_lot::Mutex; use parking_lot::RwLock; use scopeguard::defer; use smartstring::alias::String; @@ -25,10 +23,7 @@ use tauri_plugin_autostart::ManagerExt as _; pub struct Sysopt { update_sysproxy: AtomicBool, reset_sysproxy: AtomicBool, - #[cfg(not(target_os = "windows"))] - sysproxy: Arc>, - #[cfg(not(target_os = "windows"))] - autoproxy: Arc>, + inner_proxy: Arc>, guard: Arc>, } @@ -37,14 +32,8 @@ impl Default for Sysopt { Self { update_sysproxy: AtomicBool::new(false), reset_sysproxy: AtomicBool::new(false), - #[cfg(not(target_os = "windows"))] - sysproxy: Arc::new(Mutex::new(Sysproxy::default())), - #[cfg(not(target_os = "windows"))] - autoproxy: Arc::new(Mutex::new(Autoproxy::default())), - guard: Arc::new(RwLock::new(GuardMonitor::new( - GuardType::None, - Duration::from_secs(30), - ))), + inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))), + guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), } } } @@ -58,11 +47,7 @@ static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; async fn get_bypass() -> String { - let use_default = Config::verge() - .await - .latest_arc() - .use_default_bypass - .unwrap_or(true); + let use_default = Config::verge().await.latest_arc().use_default_bypass.unwrap_or(true); let res = { let verge = Config::verge().await; let verge = verge.latest_arc(); @@ -82,35 +67,6 @@ async fn get_bypass() -> String { } } -// Uses tokio Command with CREATE_NO_WINDOW flag to avoid DLL initialization issues during shutdown -#[cfg(target_os = "windows")] -async fn execute_sysproxy_command(args: Vec) -> Result<()> { - use crate::utils::dirs; - use anyhow::bail; - #[allow(unused_imports)] // Required for .creation_flags() method - use std::os::windows::process::CommandExt as _; - use tokio::process::Command; - - let binary_path = dirs::service_path()?; - let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); - - if !sysproxy_exe.exists() { - bail!("sysproxy.exe not found"); - } - - let output = Command::new(sysproxy_exe) - .args(args) - .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口 - .output() - .await?; - - if !output.status.success() { - bail!("sysproxy exe run failed"); - } - - Ok(()) -} - singleton!(Sysopt, SYSOPT); impl Sysopt { @@ -125,12 +81,12 @@ impl Sysopt { pub async fn refresh_guard(&self) { logging!(info, Type::Core, "Refreshing system proxy guard..."); let verge = Config::verge().await.latest_arc(); - if !verge.enable_system_proxy.unwrap_or(false) { + if !verge.enable_system_proxy.unwrap_or_default() { logging!(info, Type::Core, "System proxy is disabled."); self.access_guard().write().stop(); return; } - if !verge.enable_proxy_guard.unwrap_or(false) { + if !verge.enable_proxy_guard.unwrap_or_default() { logging!(info, Type::Core, "System proxy guard is disabled."); return; } @@ -142,9 +98,9 @@ impl Sysopt { ); { let guard = self.access_guard(); - guard.write().set_interval(Duration::from_secs( - verge.proxy_guard_duration.unwrap_or(30), - )); + guard + .write() + .set_interval(Duration::from_secs(verge.proxy_guard_duration.unwrap_or(30))); } logging!(info, Type::Core, "Starting system proxy guard..."); { @@ -182,100 +138,62 @@ impl Sysopt { }; let pac_port = IVerge::get_singleton_port(); - let (sys_enable, pac_enable, proxy_host) = { + let (sys_enable, pac_enable, proxy_host, proxy_guard) = { ( - verge.enable_system_proxy.unwrap_or(false), - verge.proxy_auto_config.unwrap_or(false), - verge - .proxy_host - .clone() - .unwrap_or_else(|| String::from("127.0.0.1")), + verge.enable_system_proxy.unwrap_or_default(), + verge.proxy_auto_config.unwrap_or_default(), + verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")), + verge.enable_proxy_guard.unwrap_or_default(), ) }; - #[cfg(not(target_os = "windows"))] - { - // 先 await, 避免持有锁导致的 Send 问题 - let bypass = get_bypass().await; + // 先 await, 避免持有锁导致的 Send 问题 + let bypass = get_bypass().await; - let mut sys = self.sysproxy.lock(); + let (sys, auto) = &mut *self.inner_proxy.write(); + sys.enable = false; + sys.host = proxy_host.clone().into(); + sys.port = port; + sys.bypass = bypass.into(); + + auto.enable = false; + auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); + + self.access_guard().write().set_guard_type(GuardType::None); + + if !sys_enable && !pac_enable { + // disable proxy + sys.set_system_proxy()?; + auto.set_auto_proxy()?; + return Ok(()); + } + + if pac_enable { sys.enable = false; - sys.host = proxy_host.clone().into(); - sys.port = port; - sys.bypass = bypass.into(); - - let mut auto = self.autoproxy.lock(); - auto.enable = false; - auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); - - if !sys_enable { - sys.set_system_proxy()?; - auto.set_auto_proxy()?; - self.access_guard() - .write() - .set_guard_type(GuardType::Sysproxy(sys.clone())); - return Ok(()); - } - - if pac_enable { - sys.enable = false; - auto.enable = true; - sys.set_system_proxy()?; - auto.set_auto_proxy()?; + auto.enable = true; + sys.set_system_proxy()?; + auto.set_auto_proxy()?; + if proxy_guard { self.access_guard() .write() .set_guard_type(GuardType::Autoproxy(auto.clone())); - return Ok(()); } + return Ok(()); + } - if sys_enable { - auto.enable = false; - sys.enable = true; - auto.set_auto_proxy()?; - sys.set_system_proxy()?; - drop(auto); + if sys_enable { + auto.enable = false; + sys.enable = true; + auto.set_auto_proxy()?; + sys.set_system_proxy()?; + if proxy_guard { self.access_guard() .write() .set_guard_type(GuardType::Sysproxy(sys.clone())); - drop(sys); - return Ok(()); } + return Ok(()); } - #[cfg(target_os = "windows")] - { - if !sys_enable { - self.access_guard().write().set_guard_type(GuardType::None); - return self.reset_sysproxy().await; - } - - let (args, guard_type): (Vec, GuardType) = if pac_enable { - let address = format!("http://{proxy_host}:{pac_port}/commands/pac"); - ( - vec!["pac".into(), address.clone()], - GuardType::Autoproxy(Autoproxy { - enable: true, - url: address, - }), - ) - } else { - let address = format!("{proxy_host}:{port}"); - let bypass = get_bypass().await; - let bypass_for_guard = bypass.as_str().to_owned(); - ( - vec!["global".into(), address.clone(), bypass.into()], - GuardType::Sysproxy(Sysproxy { - enable: true, - host: proxy_host.clone().into(), - port, - bypass: bypass_for_guard, - }), - ) - }; - - execute_sysproxy_command(args).await?; - self.access_guard().write().set_guard_type(guard_type); - } Ok(()) } @@ -293,22 +211,15 @@ impl Sysopt { self.reset_sysproxy.store(false, Ordering::SeqCst); } - //直接关闭所有代理 - #[cfg(not(target_os = "windows"))] - { - let mut sysproxy = self.sysproxy.lock(); - sysproxy.enable = false; - sysproxy.set_system_proxy()?; - drop(sysproxy); - let mut autoproxy = self.autoproxy.lock(); - autoproxy.enable = false; - autoproxy.set_auto_proxy()?; - } + // close proxy guard + self.access_guard().write().set_guard_type(GuardType::None); - #[cfg(target_os = "windows")] - { - execute_sysproxy_command(vec!["set".into(), "1".into()]).await?; - } + // 直接关闭所有代理 + let (sys, auto) = &mut *self.inner_proxy.write(); + sys.enable = false; + sys.set_system_proxy()?; + auto.enable = false; + auto.set_auto_proxy()?; Ok(()) } @@ -317,12 +228,7 @@ impl Sysopt { pub async fn update_launch(&self) -> Result<()> { let enable_auto_launch = { Config::verge().await.latest_arc().enable_auto_launch }; let is_enable = enable_auto_launch.unwrap_or(false); - logging!( - info, - Type::System, - "Setting auto-launch state to: {:?}", - is_enable - ); + logging!(info, Type::System, "Setting auto-launch state to: {:?}", is_enable); // 首先尝试使用快捷方式方法 #[cfg(target_os = "windows")] diff --git a/clash-verge-rev/src-tauri/src/core/timer.rs b/clash-verge-rev/src-tauri/src/core/timer.rs index 0812dddc5a..5a00b36a9e 100644 --- a/clash-verge-rev/src-tauri/src/core/timer.rs +++ b/clash-verge-rev/src-tauri/src/core/timer.rs @@ -75,12 +75,7 @@ impl Timer { // Log timer info first { let timer_map = self.timer_map.read(); - logging!( - info, - Type::Timer, - "已注册的定时任务数量: {}", - timer_map.len() - ); + logging!(info, Type::Timer, "已注册的定时任务数量: {}", timer_map.len()); for (uid, task) in timer_map.iter() { logging!( @@ -97,32 +92,30 @@ impl Timer { let cur_timestamp = chrono::Local::now().timestamp(); // Collect profiles that need immediate update - let profiles_to_update = - if let Some(items) = Config::profiles().await.latest_arc().get_items() { - items - .iter() - .filter_map(|item| { - let allow_auto_update = - item.option.as_ref()?.allow_auto_update.unwrap_or_default(); - if !allow_auto_update { - return None; - } + let profiles_to_update = if let Some(items) = Config::profiles().await.latest_arc().get_items() { + items + .iter() + .filter_map(|item| { + let allow_auto_update = item.option.as_ref()?.allow_auto_update.unwrap_or_default(); + if !allow_auto_update { + return None; + } - let interval = item.option.as_ref()?.update_interval? as i64; - let updated = item.updated? as i64; - let uid = item.uid.as_ref()?; + let interval = item.option.as_ref()?.update_interval? as i64; + let updated = item.updated? as i64; + let uid = item.uid.as_ref()?; - if interval > 0 && cur_timestamp - updated >= interval * 60 { - logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid); - Some(uid.clone()) - } else { - None - } - }) - .collect::>() - } else { - Vec::new() - }; + if interval > 0 && cur_timestamp - updated >= interval * 60 { + logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid); + Some(uid.clone()) + } else { + None + } + }) + .collect::>() + } else { + Vec::new() + }; // Advance tasks outside of locks to minimize lock contention if !profiles_to_update.is_empty() { @@ -149,25 +142,6 @@ impl Timer { Ok(()) } - /// 每 3 秒更新系统托盘菜单,总共执行 3 次 - pub fn add_update_tray_menu_task(&self) -> Result<()> { - let tid = self.timer_count.fetch_add(1, Ordering::SeqCst); - let task = TaskBuilder::default() - .set_task_id(tid) - .set_maximum_parallel_runnable_num(1) - .set_frequency_count_down_by_seconds(3, 3) - .spawn_async_routine(|| async move { - logging!(debug, Type::Timer, "Updating tray menu"); - crate::core::tray::Tray::global().update_menu().await - }) - .context("failed to create update tray menu timer task")?; - self.delay_timer - .write() - .add_task(task) - .context("failed to add update tray menu timer task")?; - Ok(()) - } - /// Refresh timer tasks with better error handling pub async fn refresh(&self) -> Result<()> { // Generate diff outside of lock to minimize lock contention @@ -178,12 +152,7 @@ impl Timer { return Ok(()); } - logging!( - info, - Type::Timer, - "Refreshing {} timer tasks", - diff_map.len() - ); + logging!(info, Type::Timer, "Refreshing {} timer tasks", diff_map.len()); // Apply changes - first collect operations to perform without holding locks let mut operations_to_add: Vec<(String, TaskID, u64)> = Vec::new(); @@ -286,12 +255,7 @@ impl Timer { } } - logging!( - debug, - Type::Timer, - "生成的定时更新配置数量: {}", - new_map.len() - ); + logging!(debug, Type::Timer, "生成的定时更新配置数量: {}", new_map.len()); new_map } @@ -302,12 +266,7 @@ impl Timer { // Read lock for comparing current state let timer_map = self.timer_map.read(); - logging!( - debug, - Type::Timer, - "当前 timer_map 大小: {}", - timer_map.len() - ); + logging!(debug, Type::Timer, "当前 timer_map 大小: {}", timer_map.len()); // Find tasks to modify or delete for (uid, task) in timer_map.iter() { @@ -364,13 +323,7 @@ impl Timer { } /// Add a timer task with better error handling - fn add_task( - &self, - delay_timer: &DelayTimer, - uid: String, - tid: TaskID, - minutes: u64, - ) -> Result<()> { + fn add_task(&self, delay_timer: &DelayTimer, uid: String, tid: TaskID, minutes: u64) -> Result<()> { logging!( info, Type::Timer, @@ -394,9 +347,7 @@ impl Timer { }) .context("failed to create timer task")?; - delay_timer - .add_task(task) - .context("failed to add timer task")?; + delay_timer.add_task(task).context("failed to add timer task")?; Ok(()) } @@ -443,13 +394,7 @@ impl Timer { // Calculate next update time if updated > 0 && task_interval > 0 { let next_time = updated + (task_interval as i64 * 60); - logging!( - info, - Type::Timer, - "计算得到下次更新时间: {}, uid={}", - next_time, - uid - ); + logging!(info, Type::Timer, "计算得到下次更新时间: {}, uid={}", next_time, uid); Some(next_time) } else { logging!( @@ -483,13 +428,7 @@ impl Timer { Self::emit_update_event(uid, true); let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid); - logging!( - info, - Type::Timer, - "配置 {} 是否为当前激活配置: {}", - uid, - is_current - ); + logging!(info, Type::Timer, "配置 {} 是否为当前激活配置: {}", uid, is_current); feat::update_profile(uid, None, is_current, false).await }) diff --git a/clash-verge-rev/src-tauri/src/core/tray/mod.rs b/clash-verge-rev/src-tauri/src/core/tray/mod.rs index 832f10d2d7..ea2891177d 100644 --- a/clash-verge-rev/src-tauri/src/core/tray/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/tray/mod.rs @@ -5,7 +5,7 @@ use tauri_plugin_mihomo::models::Proxies; use tokio::fs; #[cfg(target_os = "macos")] pub mod speed_rate; -use crate::config::{IProfilePreview, IVerge, PrfSelected}; +use crate::config::{IProfilePreview, IVerge}; use crate::core::service; use crate::module::lightweight; use crate::process::AsyncHandler; @@ -93,29 +93,17 @@ impl TrayState { } #[cfg(target_os = "macos")] { - let tray_icon_colorful = verge - .tray_icon - .clone() - .unwrap_or_else(|| "monochrome".into()); + let tray_icon_colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into()); if tray_icon_colorful == "monochrome" { - ( - false, - include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon-mono.ico").to_vec()) } else { - ( - false, - include_bytes!("../../../icons/tray-icon.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon.ico").to_vec()) } } #[cfg(not(target_os = "macos"))] { - ( - false, - include_bytes!("../../../icons/tray-icon.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon.ico").to_vec()) } } @@ -129,29 +117,20 @@ impl TrayState { } #[cfg(target_os = "macos")] { - let tray_icon_colorful = verge - .tray_icon - .clone() - .unwrap_or_else(|| "monochrome".into()); + let tray_icon_colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into()); if tray_icon_colorful == "monochrome" { ( false, include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(), ) } else { - ( - false, - include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon-sys.ico").to_vec()) } } #[cfg(not(target_os = "macos"))] { - ( - false, - include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon-sys.ico").to_vec()) } } @@ -165,28 +144,19 @@ impl TrayState { } #[cfg(target_os = "macos")] { - let tray_icon_colorful = verge - .tray_icon - .clone() - .unwrap_or_else(|| "monochrome".into()); + let tray_icon_colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into()); if tray_icon_colorful == "monochrome" { ( false, include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(), ) } else { - ( - false, - include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon-tun.ico").to_vec()) } } #[cfg(not(target_os = "macos"))] { - ( - false, - include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(), - ) + (false, include_bytes!("../../../icons/tray-icon-tun.ico").to_vec()) } } } @@ -228,8 +198,6 @@ impl Tray { ); } } - // TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白 - crate::core::timer::Timer::global().add_update_tray_menu_task()?; Ok(()) } @@ -304,8 +272,8 @@ impl Tray { let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let tun_mode_available = is_current_app_handle_admin(app_handle) - || service::is_service_available().await.is_ok(); + let tun_mode_available = + is_current_app_handle_admin(app_handle) || service::is_service_available().await.is_ok(); let mode = { Config::clash() .await @@ -339,11 +307,7 @@ impl Tray { Ok(()) } None => { - logging!( - warn, - Type::Tray, - "Failed to update tray menu: tray not found" - ); + logging!(warn, Type::Tray, "Failed to update tray menu: tray not found"); Ok(()) } } @@ -362,11 +326,7 @@ impl Tray { let tray = match app_handle.tray_by_id("main") { Some(tray) => tray, None => { - logging!( - warn, - Type::Tray, - "Failed to update tray icon: tray not found" - ); + logging!(warn, Type::Tray, "Failed to update tray icon: tray not found"); return Ok(()); } }; @@ -381,10 +341,7 @@ impl Tray { (false, false) => TrayState::get_common_tray_icon(verge).await, }; - let colorful = verge - .tray_icon - .clone() - .unwrap_or_else(|| "monochrome".into()); + let colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into()); let is_colorful = colorful == "colorful"; let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); @@ -404,11 +361,7 @@ impl Tray { let tray = match app_handle.tray_by_id("main") { Some(tray) => tray, None => { - logging!( - warn, - Type::Tray, - "Failed to update tray icon: tray not found" - ); + logging!(warn, Type::Tray, "Failed to update tray icon: tray not found"); return Ok(()); } }; @@ -488,11 +441,7 @@ impl Tray { if let Some(tray) = app_handle.tray_by_id("main") { let _ = tray.set_tooltip(Some(&tooltip)); } else { - logging!( - warn, - Type::Tray, - "Failed to update tray tooltip: tray not found" - ); + logging!(warn, Type::Tray, "Failed to update tray tooltip: tray not found"); } Ok(()) @@ -525,22 +474,22 @@ impl Tray { let icon = tauri::image::Image::from_bytes(&icon_bytes)?; #[cfg(target_os = "linux")] - let builder = TrayIconBuilder::with_id("main") - .icon(icon) - .icon_as_template(false); + let builder = TrayIconBuilder::with_id("main").icon(icon).icon_as_template(false); #[cfg(any(target_os = "macos", target_os = "windows"))] let show_menu_on_left_click = { // TODO 优化这里 复用 verge let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; - let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); - tray_event.as_str() == "tray_menu" + tray_event.is_some_and(|v| v == "tray_menu") }; #[cfg(not(target_os = "linux"))] - let mut builder = TrayIconBuilder::with_id("main") - .icon(icon) - .icon_as_template(false); + let mut builder = TrayIconBuilder::with_id("main").icon(icon).icon_as_template(false); + #[cfg(target_os = "macos")] + { + let is_monochrome = verge.tray_icon.clone().is_none_or(|v| v == "monochrome"); + builder = builder.icon_as_template(is_monochrome); + } #[cfg(any(target_os = "macos", target_os = "windows"))] { @@ -558,16 +507,16 @@ impl Tray { .. } = event { + // 添加防抖检查,防止快速连击 + if !should_handle_tray_click() { + logging!(info, Type::Tray, "click tray icon too fast, ignore"); + return; + } AsyncHandler::spawn(|| async move { let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); logging!(debug, Type::Tray, "tray event: {tray_event:?}"); - // 添加防抖检查,防止快速连击 - if !should_handle_tray_click() { - return; - } - match tray_event.as_str() { "system_proxy" => feat::toggle_system_proxy().await, "tun_mode" => feat::toggle_tun_mode(None).await, @@ -576,7 +525,9 @@ impl Tray { WindowManager::show_main_window().await; }; } - _ => {} + _ => { + logging!(warn, Type::Tray, "invalid tray event: {}", tray_event); + } }; }); } @@ -633,23 +584,20 @@ fn create_profile_menu_item( fn create_subcreate_proxy_menu_item( app_handle: &AppHandle, proxy_mode: &str, - current_profile_selected: &[PrfSelected], proxy_group_order_map: Option>, - proxy_nodes_data: Result, + proxy_nodes_data: Option, ) -> Vec> { let proxy_submenus: Vec> = { let mut submenus: Vec<(String, usize, Submenu)> = Vec::new(); // TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息 - if let Ok(proxy_nodes_data) = proxy_nodes_data { + if let Some(proxy_nodes_data) = proxy_nodes_data { for (group_name, group_data) in proxy_nodes_data.proxies.iter() { - // Filter groups based on mode + // Filter groups based on mode and hidden flag let should_show = match proxy_mode { "global" => group_name == "GLOBAL", _ => group_name != "GLOBAL", - } && - // Check if the group is hidden - !group_data.hidden.unwrap_or_default(); + } && !group_data.hidden.unwrap_or_default(); if !should_show { continue; @@ -682,18 +630,9 @@ fn create_subcreate_proxy_menu_item( let display_text = format!("{} | {}", proxy_str, delay_text); - CheckMenuItem::with_id( - app_handle, - item_id, - display_text, - true, - is_selected, - None::<&str>, - ) - .map_err(|e| { - logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e) - }) - .ok() + CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>) + .map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)) + .ok() }) .collect(); @@ -701,28 +640,10 @@ fn create_subcreate_proxy_menu_item( continue; } - // Determine if group is active - let is_group_active = match proxy_mode { - "global" => group_name == "GLOBAL" && !now_proxy.is_empty(), - "direct" => false, - _ => { - current_profile_selected - .iter() - .any(|s| s.name.as_deref() == Some(group_name)) - && !now_proxy.is_empty() - } - }; + let group_display_name = group_name.to_string(); - let group_display_name = if is_group_active { - format!("✓ {}", group_name) - } else { - group_name.to_string() - }; - - let group_items_refs: Vec<&dyn IsMenuItem> = group_items - .iter() - .map(|item| item as &dyn IsMenuItem) - .collect(); + let group_items_refs: Vec<&dyn IsMenuItem> = + group_items.iter().map(|item| item as &dyn IsMenuItem).collect(); if let Ok(submenu) = Submenu::with_id_and_items( app_handle, @@ -734,34 +655,23 @@ fn create_subcreate_proxy_menu_item( let insertion_index = submenus.len(); submenus.push((group_name.into(), insertion_index, submenu)); } else { - logging!( - warn, - Type::Tray, - "Failed to create proxy group submenu: {}", - group_name - ); + logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name); } } } if let Some(order_map) = proxy_group_order_map.as_ref() { - submenus.sort_by( - |(name_a, original_index_a, _), (name_b, original_index_b, _)| match ( - order_map.get(name_a), - order_map.get(name_b), - ) { + submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| { + match (order_map.get(name_a), order_map.get(name_b)) { (Some(index_a), Some(index_b)) => index_a.cmp(index_b), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => original_index_a.cmp(original_index_b), - }, - ); + } + }); } - submenus - .into_iter() - .map(|(_, _, submenu)| submenu) - .collect() + submenus.into_iter().map(|(_, _, submenu)| submenu).collect() }; proxy_submenus } @@ -816,18 +726,13 @@ async fn create_tray_menu( i18n::sync_locale().await; - // 获取当前配置文件的选中代理组信息 - let current_profile_selected = { - let profiles_config = Config::profiles().await; - let profiles_ref = profiles_config.latest_arc(); - profiles_ref - .get_current() - .and_then(|uid| profiles_ref.get_item(uid).ok()) - .and_then(|profile| profile.selected.clone()) - .unwrap_or_default() - }; - - let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await; + // TODO: should update tray menu again when it was timeout error + let proxy_nodes_data = tokio::time::timeout( + Duration::from_millis(1000), + handle::Handle::mihomo().await.get_proxies(), + ) + .await + .map_or(None, |res| res.ok()); let runtime_proxy_groups_order = cmd::get_runtime_config() .await @@ -855,15 +760,14 @@ async fn create_tray_menu( .unwrap_or_default() }); - let proxy_group_order_map: Option< - HashMap, usize>, - > = runtime_proxy_groups_order.as_ref().map(|group_names| { - group_names - .iter() - .enumerate() - .map(|(index, name)| (name.clone(), index)) - .collect::>() - }); + let proxy_group_order_map: Option, usize>> = + runtime_proxy_groups_order.as_ref().map(|group_names| { + group_names + .iter() + .enumerate() + .map(|(index, name)| (name.clone(), index)) + .collect::>() + }); let verge_settings = Config::verge().await.latest_arc(); let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(true); @@ -872,8 +776,7 @@ async fn create_tray_menu( let hotkeys = create_hotkeys(&verge_settings.hotkeys); - let profile_menu_items: Vec> = - create_profile_menu_item(app_handle, profiles_preview)?; + let profile_menu_items: Vec> = create_profile_menu_item(app_handle, profiles_preview)?; // Pre-fetch all localized strings let texts = MenuTexts::new(); @@ -945,20 +848,11 @@ async fn create_tray_menu( &profile_menu_items_refs, )?; - let proxy_sub_menus = create_subcreate_proxy_menu_item( - app_handle, - current_proxy_mode, - ¤t_profile_selected, - proxy_group_order_map, - proxy_nodes_data.map_err(anyhow::Error::from), - ); + let proxy_sub_menus = + create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data); - let (proxies_menu, inline_proxy_items) = create_proxy_menu_item( - app_handle, - show_proxy_groups_inline, - proxy_sub_menus, - &texts.proxies, - )?; + let (proxies_menu, inline_proxy_items) = + create_proxy_menu_item(app_handle, show_proxy_groups_inline, proxy_sub_menus, &texts.proxies)?; let system_proxy = &CheckMenuItem::with_id( app_handle, @@ -995,66 +889,24 @@ async fn create_tray_menu( hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()), )?; - let copy_env = &MenuItem::with_id( - app_handle, - MenuIds::COPY_ENV, - &texts.copy_env, - true, - None::<&str>, - )?; + let copy_env = &MenuItem::with_id(app_handle, MenuIds::COPY_ENV, &texts.copy_env, true, None::<&str>)?; - let open_app_dir = &MenuItem::with_id( - app_handle, - MenuIds::CONF_DIR, - &texts.conf_dir, - true, - None::<&str>, - )?; + let open_app_dir = &MenuItem::with_id(app_handle, MenuIds::CONF_DIR, &texts.conf_dir, true, None::<&str>)?; - let open_core_dir = &MenuItem::with_id( - app_handle, - MenuIds::CORE_DIR, - &texts.core_dir, - true, - None::<&str>, - )?; + let open_core_dir = &MenuItem::with_id(app_handle, MenuIds::CORE_DIR, &texts.core_dir, true, None::<&str>)?; - let open_logs_dir = &MenuItem::with_id( - app_handle, - MenuIds::LOGS_DIR, - &texts.logs_dir, - true, - None::<&str>, - )?; + let open_logs_dir = &MenuItem::with_id(app_handle, MenuIds::LOGS_DIR, &texts.logs_dir, true, None::<&str>)?; - let open_app_log = &MenuItem::with_id( - app_handle, - MenuIds::APP_LOG, - &texts.app_log, - true, - None::<&str>, - )?; + let open_app_log = &MenuItem::with_id(app_handle, MenuIds::APP_LOG, &texts.app_log, true, None::<&str>)?; - let open_core_log = &MenuItem::with_id( - app_handle, - MenuIds::CORE_LOG, - &texts.core_log, - true, - None::<&str>, - )?; + let open_core_log = &MenuItem::with_id(app_handle, MenuIds::CORE_LOG, &texts.core_log, true, None::<&str>)?; let open_dir = &Submenu::with_id_and_items( app_handle, MenuIds::OPEN_DIR, &texts.open_dir, true, - &[ - open_app_dir, - open_core_dir, - open_logs_dir, - open_app_log, - open_core_log, - ], + &[open_app_dir, open_core_dir, open_logs_dir, open_app_log, open_core_log], )?; let restart_clash = &MenuItem::with_id( @@ -1065,13 +917,7 @@ async fn create_tray_menu( None::<&str>, )?; - let restart_app = &MenuItem::with_id( - app_handle, - MenuIds::RESTART_APP, - &texts.restart_app, - true, - None::<&str>, - )?; + let restart_app = &MenuItem::with_id(app_handle, MenuIds::RESTART_APP, &texts.restart_app, true, None::<&str>)?; let app_version = &MenuItem::with_id( app_handle, @@ -1095,19 +941,12 @@ async fn create_tray_menu( ], )?; - let quit = &MenuItem::with_id( - app_handle, - MenuIds::EXIT, - &texts.exit, - true, - Some("CmdOrControl+Q"), - )?; + let quit = &MenuItem::with_id(app_handle, MenuIds::EXIT, &texts.exit, true, Some("CmdOrControl+Q"))?; let separator = &PredefinedMenuItem::separator(app_handle)?; // 动态构建菜单项 - let mut menu_items: Vec<&dyn IsMenuItem> = - vec![open_window, outbound_modes, separator, profiles]; + let mut menu_items: Vec<&dyn IsMenuItem> = vec![open_window, outbound_modes, separator, profiles]; // 如果有代理节点,添加代理节点菜单 if show_proxy_groups_inline { @@ -1130,9 +969,7 @@ async fn create_tray_menu( quit as &dyn IsMenuItem, ]); - let menu = tauri::menu::MenuBuilder::new(app_handle) - .items(&menu_items) - .build()?; + let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?; Ok(menu) } @@ -1140,7 +977,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { AsyncHandler::spawn(|| async move { match event.id.as_ref() { mode @ (MenuIds::RULE_MODE | MenuIds::GLOBAL_MODE | MenuIds::DIRECT_MODE) => { - // Removing the the "tray_" preffix and "_mode" suffix + // Removing the the "tray_" prefix and "_mode" suffix let mode = &mode[5..mode.len() - 5]; logging!(info, Type::ProxyMode, "Switch Proxy Mode To: {}", mode); feat::change_clash_mode(mode.into()).await; @@ -1163,11 +1000,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { } MenuIds::CLOSE_ALL_CONNECTIONS => { if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { - logging!( - error, - Type::Tray, - "Failed to close all connections from tray: {err}" - ); + logging!(error, Type::Tray, "Failed to close all connections from tray: {err}"); } } MenuIds::COPY_ENV => feat::copy_clash_env().await, @@ -1219,12 +1052,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { feat::switch_proxy_node(group_name, proxy_name).await; } _ => { - logging!( - debug, - Type::Tray, - "Unhandled tray menu event: {:?}", - event.id - ); + logging!(debug, Type::Tray, "Unhandled tray menu event: {:?}", event.id); } } diff --git a/clash-verge-rev/src-tauri/src/core/validate.rs b/clash-verge-rev/src-tauri/src/core/validate.rs index 153daa118b..5451225355 100644 --- a/clash-verge-rev/src-tauri/src/core/validate.rs +++ b/clash-verge-rev/src-tauri/src/core/validate.rs @@ -45,17 +45,8 @@ impl CoreConfigValidator { let content = match fs::read_to_string(path).await { Ok(content) => content, Err(err) => { - logging!( - warn, - Type::Validate, - "无法读取文件以检测类型: {}, 错误: {}", - path, - err - ); - return Err(anyhow::anyhow!( - "Failed to read file to detect type: {}", - err - )); + logging!(warn, Type::Validate, "无法读取文件以检测类型: {}, 错误: {}", path, err); + return Err(anyhow::anyhow!("Failed to read file to detect type: {}", err)); } }; @@ -102,12 +93,7 @@ impl CoreConfigValidator { } // 默认情况:无法确定时,假设为非脚本文件(更安全) - logging!( - debug, - Type::Validate, - "无法确定文件类型,默认当作YAML处理: {}", - path - ); + logging!(debug, Type::Validate, "无法确定文件类型,默认当作YAML处理: {}", path); Ok(false) } @@ -188,10 +174,7 @@ impl CoreConfigValidator { } /// 验证指定的配置文件 - pub async fn validate_config_file( - config_path: &str, - is_merge_file: Option, - ) -> Result<(bool, String)> { + pub async fn validate_config_file(config_path: &str, is_merge_file: Option) -> Result<(bool, String)> { // 检查程序是否正在退出,如果是则跳过验证 if handle::Handle::global().is_exiting() { logging!(info, Type::Core, "应用正在退出,跳过验证"); @@ -207,12 +190,7 @@ impl CoreConfigValidator { // 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证 if is_merge_file.unwrap_or(false) { - logging!( - info, - Type::Validate, - "检测到Merge文件,仅进行语法检查: {}", - config_path - ); + logging!(info, Type::Validate, "检测到Merge文件,仅进行语法检查: {}", config_path); return Self::validate_file_syntax(config_path).await; } @@ -224,13 +202,7 @@ impl CoreConfigValidator { Ok(result) => result, Err(err) => { // 如果无法确定文件类型,尝试使用Clash内核验证 - logging!( - warn, - Type::Validate, - "无法确定文件类型: {}, 错误: {}", - config_path, - err - ); + logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err); return Self::validate_config_internal(config_path).await; } } @@ -247,12 +219,7 @@ impl CoreConfigValidator { } // 对YAML配置文件使用Clash内核验证 - logging!( - info, - Type::Validate, - "使用Clash内核验证配置文件: {}", - config_path - ); + logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path); Self::validate_config_internal(config_path).await } @@ -275,13 +242,11 @@ impl CoreConfigValidator { logging!(info, Type::Validate, "验证目录: {}", app_dir_str); // 使用子进程运行clash验证配置 - let command = app_handle.shell().sidecar(clash_core.as_str())?.args([ - "-t", - "-d", - app_dir_str, - "-f", - config_path, - ]); + let command = + app_handle + .shell() + .sidecar(clash_core.as_str())? + .args(["-t", "-d", app_dir_str, "-f", config_path]); let output = command.output().await?; let status = &output.status; diff --git a/clash-verge-rev/src-tauri/src/enhance/chain.rs b/clash-verge-rev/src-tauri/src/enhance/chain.rs index efbcf216d6..c2132e84e4 100644 --- a/clash-verge-rev/src-tauri/src/enhance/chain.rs +++ b/clash-verge-rev/src-tauri/src/enhance/chain.rs @@ -118,19 +118,16 @@ impl ChainItem { /// 内建支持一些脚本 pub fn builtin() -> Vec<(ChainSupport, Self)> { // meta 的一些处理 - let meta_guard = - Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); + let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); // meta 1.13.2 alpn string 转 数组 let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); // meta 的一些处理 - let meta_guard_alpha = - Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); + let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); // meta 1.13.2 alpn string 转 数组 - let hy_alpn_alpha = - Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); + let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); vec![ (ChainSupport::ClashMeta, hy_alpn), diff --git a/clash-verge-rev/src-tauri/src/enhance/field.rs b/clash-verge-rev/src-tauri/src/enhance/field.rs index 72f9e916bd..3d841df345 100644 --- a/clash-verge-rev/src-tauri/src/enhance/field.rs +++ b/clash-verge-rev/src-tauri/src/enhance/field.rs @@ -17,13 +17,7 @@ pub const HANDLE_FIELDS: [&str; 12] = [ "unified-delay", ]; -pub const DEFAULT_FIELDS: [&str; 5] = [ - "proxies", - "proxy-providers", - "proxy-groups", - "rule-providers", - "rules", -]; +pub const DEFAULT_FIELDS: [&str; 5] = ["proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"]; pub fn use_lowercase(config: &Mapping) -> Mapping { let mut ret = Mapping::new(); diff --git a/clash-verge-rev/src-tauri/src/enhance/mod.rs b/clash-verge-rev/src-tauri/src/enhance/mod.rs index f8e8ba05cf..99bfaafc1a 100644 --- a/clash-verge-rev/src-tauri/src/enhance/mod.rs +++ b/clash-verge-rev/src-tauri/src/enhance/mod.rs @@ -455,11 +455,7 @@ async fn merge_default_config( config } -fn apply_builtin_scripts( - mut config: Mapping, - clash_core: Option, - enable_builtin: bool, -) -> Mapping { +fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable_builtin: bool) -> Mapping { if enable_builtin { ChainItem::builtin() .into_iter() @@ -552,9 +548,7 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { if let Some(Value::Sequence(proxies)) = group_map.get_mut("proxies") { proxies.retain(|proxy| match proxy { - Value::String(name) => { - allowed_names.contains(name.as_str()) || has_valid_provider - } + Value::String(name) => allowed_names.contains(name.as_str()) || has_valid_provider, _ => true, }); } @@ -627,8 +621,7 @@ pub async fn enhance() -> (Mapping, HashSet, HashMap) let profile_name = profile.profile_name; // process globals - let (config, exists_keys, result_map) = - process_global_items(config, global_merge, global_script, &profile_name); + let (config, exists_keys, result_map) = process_global_items(config, global_merge, global_script, &profile_name); // process profile-specific items let (config, exists_keys, result_map) = process_profile_items( @@ -710,9 +703,7 @@ proxy-groups: let manual_group = groups .iter() - .find(|group| { - group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") - }) + .find(|group| group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual")) .and_then(|group| group.as_mapping()) .expect("manual group should exist"); @@ -722,18 +713,12 @@ proxy-groups: .expect("manual proxies should be a sequence"); assert_eq!(manual_proxies.len(), 2); - assert!( - manual_proxies - .iter() - .any(|p| p.as_str() == Some("alive-node")) - ); + assert!(manual_proxies.iter().any(|p| p.as_str() == Some("alive-node"))); assert!(manual_proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); let nested_group = groups .iter() - .find(|group| { - group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("nested") - }) + .find(|group| group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("nested")) .and_then(|group| group.as_mapping()) .expect("nested group should exist"); @@ -778,9 +763,7 @@ proxy-groups: let manual_group = groups .iter() - .find(|group| { - group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") - }) + .find(|group| group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual")) .and_then(|group| group.as_mapping()) .expect("manual group should exist"); @@ -825,9 +808,7 @@ proxy-groups: let manual_group = groups .iter() - .find(|group| { - group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") - }) + .find(|group| group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual")) .and_then(|group| group.as_mapping()) .expect("manual group should exist"); diff --git a/clash-verge-rev/src-tauri/src/enhance/script.rs b/clash-verge-rev/src-tauri/src/enhance/script.rs index d1c19184ba..df8d3f638b 100644 --- a/clash-verge-rev/src-tauri/src/enhance/script.rs +++ b/clash-verge-rev/src-tauri/src/enhance/script.rs @@ -12,11 +12,7 @@ const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB const MAX_JSON_SIZE: usize = 10 * 1024 * 1024; // 10MB // TODO 使用引用改进上下相关处理,避免不必要 Clone -pub fn use_script( - script: String, - config: &Mapping, - name: &String, -) -> Result<(Mapping, Vec<(String, String)>)> { +pub fn use_script(script: String, config: &Mapping, name: &String) -> Result<(Mapping, Vec<(String, String)>)> { let mut context = Context::default(); let outputs = Arc::new(Mutex::new(vec![])); @@ -26,50 +22,42 @@ pub fn use_script( let total_size_clone = Arc::clone(&total_size); let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe { - NativeFunction::from_closure( - move |_: &JsValue, args: &[JsValue], context: &mut Context| { - let level = args.first().ok_or_else(|| { - boa_engine::JsError::from_opaque( - JsString::from("Missing level argument").into(), - ) - })?; - let level = level.to_string(context)?; - let level = level.to_std_string().map_err(|_| { - boa_engine::JsError::from_opaque( - JsString::from("Failed to convert level to string").into(), - ) - })?; + NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| { + let level = args + .first() + .ok_or_else(|| boa_engine::JsError::from_opaque(JsString::from("Missing level argument").into()))?; + let level = level.to_string(context)?; + let level = level.to_std_string().map_err(|_| { + boa_engine::JsError::from_opaque(JsString::from("Failed to convert level to string").into()) + })?; - let data = args.get(1).ok_or_else(|| { - boa_engine::JsError::from_opaque(JsString::from("Missing data argument").into()) - })?; - let data = data.to_string(context)?; - let data = data.to_std_string().map_err(|_| { - boa_engine::JsError::from_opaque( - JsString::from("Failed to convert data to string").into(), - ) - })?; + let data = args + .get(1) + .ok_or_else(|| boa_engine::JsError::from_opaque(JsString::from("Missing data argument").into()))?; + let data = data.to_string(context)?; + let data = data.to_std_string().map_err(|_| { + boa_engine::JsError::from_opaque(JsString::from("Failed to convert data to string").into()) + })?; - // 检查输出限制 - if outputs_clone.lock().len() >= MAX_OUTPUTS { - return Err(boa_engine::JsError::from_opaque( - JsString::from("Maximum number of log outputs exceeded").into(), - )); - } + // 检查输出限制 + if outputs_clone.lock().len() >= MAX_OUTPUTS { + return Err(boa_engine::JsError::from_opaque( + JsString::from("Maximum number of log outputs exceeded").into(), + )); + } - let mut size = total_size_clone.lock(); - let new_size = *size + level.len() + data.len(); - if new_size > MAX_OUTPUT_SIZE { - return Err(boa_engine::JsError::from_opaque( - JsString::from("Maximum output size exceeded").into(), - )); - } - *size = new_size; - drop(size); - outputs_clone.lock().push((level.into(), data.into())); - Ok(JsValue::undefined()) - }, - ) + let mut size = total_size_clone.lock(); + let new_size = *size + level.len() + data.len(); + if new_size > MAX_OUTPUT_SIZE { + return Err(boa_engine::JsError::from_opaque( + JsString::from("Maximum output size exceeded").into(), + )); + } + *size = new_size; + drop(size); + outputs_clone.lock().push((level.into(), data.into())); + Ok(JsValue::undefined()) + }) }); let _ = context.eval(Source::from_bytes( @@ -127,12 +115,7 @@ pub fn use_script( outputs .lock() .push(("exception".into(), "Script execution failed".into())); - logging_error!( - Type::Config, - "Script execution error: {}. Script name: {}", - err, - name - ); + logging_error!(Type::Config, "Script execution error: {}. Script name: {}", err, name); Ok((config, outputs.lock().to_vec())) } } @@ -205,8 +188,8 @@ fn test_script() { "; let config = &serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML"); - let (config, results) = use_script(script.into(), config, &String::from("")) - .expect("Script execution should succeed in test"); + let (config, results) = + use_script(script.into(), config, &String::from("")).expect("Script execution should succeed in test"); let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML"); let yaml_config_size = std::mem::size_of_val(&config); @@ -230,8 +213,7 @@ fn test_escape_unescape() { assert!(parsed.contains_key("nested")); let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#; - let parsed_quoted = - parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely"); + let parsed_quoted = parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely"); assert!(parsed_quoted.contains_key("key")); assert!(parsed_quoted.contains_key("nested")); diff --git a/clash-verge-rev/src-tauri/src/enhance/seq.rs b/clash-verge-rev/src-tauri/src/enhance/seq.rs index c978fc76b2..78092f7523 100644 --- a/clash-verge-rev/src-tauri/src/enhance/seq.rs +++ b/clash-verge-rev/src-tauri/src/enhance/seq.rs @@ -62,20 +62,14 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { }) .cloned() .collect(); - group_map.insert( - Value::String("proxies".into()), - Value::Sequence(filtered_proxies), - ); + group_map.insert(Value::String("proxies".into()), Value::Sequence(filtered_proxies)); } new_groups.push(Value::Mapping(group_map.to_owned())); } else { new_groups.push(group.to_owned()); } } - config.insert( - Value::String("proxy-groups".into()), - Value::Sequence(new_groups), - ); + config.insert(Value::String("proxy-groups".into()), Value::Sequence(new_groups)); } config @@ -108,8 +102,7 @@ proxy-groups: proxies: - "proxy1" "#; - let mut config: Mapping = - serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML"); + let mut config: Mapping = serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML"); let seq = SeqMap { prepend: Sequence::new(), @@ -160,9 +153,7 @@ proxy-groups: assert_eq!(group1_proxies.len(), 1); assert_eq!( - group1_proxies[0] - .as_str() - .expect("proxy name should be string"), + group1_proxies[0].as_str().expect("proxy name should be string"), "proxy2" ); assert_eq!(group2_proxies.len(), 0); diff --git a/clash-verge-rev/src-tauri/src/enhance/tun.rs b/clash-verge-rev/src-tauri/src/enhance/tun.rs index dc78dc2685..5ac495307d 100644 --- a/clash-verge-rev/src-tauri/src/enhance/tun.rs +++ b/clash-verge-rev/src-tauri/src/enhance/tun.rs @@ -36,10 +36,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { val.as_mapping().cloned().unwrap_or_else(Mapping::new) }); let ipv6_key = Value::from("ipv6"); - let ipv6_val = config - .get(&ipv6_key) - .and_then(|v| v.as_bool()) - .unwrap_or(false); + let ipv6_val = config.get(&ipv6_key).and_then(|v| v.as_bool()).unwrap_or(false); // 检查现有的 enhanced-mode 设置 let current_mode = dns_val diff --git a/clash-verge-rev/src-tauri/src/feat/backup.rs b/clash-verge-rev/src-tauri/src/feat/backup.rs index a155bafc6d..23d9060811 100644 --- a/clash-verge-rev/src-tauri/src/feat/backup.rs +++ b/clash-verge-rev/src-tauri/src/feat/backup.rs @@ -47,11 +47,7 @@ async fn finalize_restored_verge_config( // Ensure side-effects (flags, tray, sysproxy, hotkeys, auto-backup refresh, etc.) run. // Use not_save_file = true to avoid extra I/O (we already persisted the restored file). if let Err(err) = super::patch_verge(&restored, true).await { - logging!( - error, - Type::Backup, - "Failed to apply restored verge config: {err:#?}" - ); + logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#?}"); } Ok(()) } @@ -83,28 +79,17 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> { /// List WebDAV backups pub async fn list_wevdav_backup() -> Result> { backup::WebDavClient::global().list().await.map_err(|err| { - logging!( - error, - Type::Backup, - "Failed to list WebDAV backup files: {err:#?}" - ); + logging!(error, Type::Backup, "Failed to list WebDAV backup files: {err:#?}"); err }) } /// Delete WebDAV backup pub async fn delete_webdav_backup(filename: String) -> Result<()> { - backup::WebDavClient::global() - .delete(filename) - .await - .map_err(|err| { - logging!( - error, - Type::Backup, - "Failed to delete WebDAV backup file: {err:#?}" - ); - err - }) + backup::WebDavClient::global().delete(filename).await.map_err(|err| { + logging!(error, Type::Backup, "Failed to delete WebDAV backup file: {err:#?}"); + err + }) } /// Restore WebDAV backup @@ -122,11 +107,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { .download(filename, backup_storage_path.clone()) .await .map_err(|err| { - logging!( - error, - Type::Backup, - "Failed to download WebDAV backup file: {err:#?}" - ); + logging!(error, Type::Backup, "Failed to download WebDAV backup file: {err:#?}"); err })?; @@ -153,11 +134,7 @@ where F: FnOnce(&str) -> String, { let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { - logging!( - error, - Type::Backup, - "Failed to create local backup: {err:#?}" - ); + logging!(error, Type::Backup, "Failed to create local backup: {err:#?}"); err })?; @@ -166,11 +143,7 @@ where let target_path = backup_dir.join(final_name.as_str()); if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await { - logging!( - error, - Type::Backup, - "Failed to move local backup file: {err:#?}" - ); + logging!(error, Type::Backup, "Failed to move local backup file: {err:#?}"); // 清理临时文件 if let Err(clean_err) = temp_file_path.remove_if_exists().await { logging!( @@ -251,12 +224,7 @@ pub async fn delete_local_backup(filename: String) -> Result<()> { let backup_dir = local_backup_dir()?; let target_path = backup_dir.join(filename.as_str()); if !target_path.exists() { - logging!( - warn, - Type::Backup, - "Local backup file not found: {}", - filename - ); + logging!(warn, Type::Backup, "Local backup file not found: {}", filename); return Ok(()); } target_path.remove_if_exists().await?; diff --git a/clash-verge-rev/src-tauri/src/feat/clash.rs b/clash-verge-rev/src-tauri/src/feat/clash.rs index 5142791ac2..b7fe4d6861 100644 --- a/clash-verge-rev/src-tauri/src/feat/clash.rs +++ b/clash-verge-rev/src-tauri/src/feat/clash.rs @@ -75,16 +75,10 @@ pub async fn change_clash_mode(mode: String) { "mode": mode }); logging!(debug, Type::Core, "change clash mode to {mode}"); - match handle::Handle::mihomo() - .await - .patch_base_config(&json_value) - .await - { + match handle::Handle::mihomo().await.patch_base_config(&json_value).await { Ok(_) => { // 更新订阅 - Config::clash() - .await - .edit_draft(|d| d.patch_config(&mapping)); + Config::clash().await.edit_draft(|d| d.patch_config(&mapping)); // 分离数据获取和异步调用 let clash_data = Config::clash().await.data_arc(); @@ -99,11 +93,7 @@ pub async fn change_clash_mode(mode: String) { ); } - let is_auto_close_connection = Config::verge() - .await - .data_arc() - .auto_close_connection - .unwrap_or(false); + let is_auto_close_connection = Config::verge().await.data_arc().auto_close_connection.unwrap_or(false); if is_auto_close_connection { after_change_clash_mode(); } @@ -117,11 +107,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { use crate::utils::network::{NetworkManager, ProxyType}; use tokio::time::Instant; - let tun_mode = Config::verge() - .await - .latest_arc() - .enable_tun_mode - .unwrap_or(false); + let tun_mode = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false); // 如果是TUN模式,不使用代理,否则使用自身代理 let proxy_type = if !tun_mode { diff --git a/clash-verge-rev/src-tauri/src/feat/config.rs b/clash-verge-rev/src-tauri/src/feat/config.rs index 07ca1a2e95..3bca539b8e 100644 --- a/clash-verge-rev/src-tauri/src/feat/config.rs +++ b/clash-verge-rev/src-tauri/src/feat/config.rs @@ -27,9 +27,7 @@ pub async fn patch_clash(patch: &Mapping) -> Result<()> { .await ); } - Config::runtime() - .await - .edit_draft(|d| d.patch_config(patch)); + Config::runtime().await.edit_draft(|d| d.patch_config(patch)); CoreManager::global().update_config().await?; } handle::Handle::refresh_clash(); @@ -257,10 +255,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { return Err(err); } Config::verge().await.apply(); - logging_error!( - Type::Backup, - AutoBackupManager::global().refresh_settings().await - ); + logging_error!(Type::Backup, AutoBackupManager::global().refresh_settings().await); if !not_save_file { // 分离数据获取和异步调用 let verge_data = Config::verge().await.data_arc(); diff --git a/clash-verge-rev/src-tauri/src/feat/profile.rs b/clash-verge-rev/src-tauri/src/feat/profile.rs index 932cafdd7c..00891d1540 100644 --- a/clash-verge-rev/src-tauri/src/feat/profile.rs +++ b/clash-verge-rev/src-tauri/src/feat/profile.rs @@ -23,13 +23,7 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) { .await { Ok(_) => { - logging!( - info, - Type::Tray, - "切换代理成功: {} -> {}", - group_name, - proxy_name - ); + logging!(info, Type::Tray, "切换代理成功: {} -> {}", group_name, proxy_name); let _ = handle::Handle::app_handle().emit("verge://refresh-proxy-config", ()); let _ = tray::Tray::global().update_menu().await; return; @@ -52,13 +46,7 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) { .await { Ok(_) => { - logging!( - info, - Type::Tray, - "代理切换回退成功: {} -> {}", - group_name, - proxy_name - ); + logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name); let _ = tray::Tray::global().update_menu().await; } Err(err) => { @@ -74,42 +62,20 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) { } } -async fn should_update_profile( - uid: &String, - ignore_auto_update: bool, -) -> Result)>> { +async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result)>> { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); let item = profiles.get_item(uid)?; let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); if !is_remote { - logging!( - info, - Type::Config, - "[订阅更新] {uid} 不是远程订阅,跳过更新" - ); + logging!(info, Type::Config, "[订阅更新] {uid} 不是远程订阅,跳过更新"); Ok(None) } else if item.url.is_none() { - logging!( - warn, - Type::Config, - "Warning: [订阅更新] {uid} 缺少URL,无法更新" - ); + logging!(warn, Type::Config, "Warning: [订阅更新] {uid} 缺少URL,无法更新"); bail!("failed to get the profile item url"); - } else if !ignore_auto_update - && !item - .option - .as_ref() - .and_then(|o| o.allow_auto_update) - .unwrap_or(true) - { - logging!( - info, - Type::Config, - "[订阅更新] {} 禁止自动更新,跳过更新", - uid - ); + } else if !ignore_auto_update && !item.option.as_ref().and_then(|o| o.allow_auto_update).unwrap_or(true) { + logging!(info, Type::Config, "[订阅更新] {} 禁止自动更新,跳过更新", uid); Ok(None) } else { logging!( @@ -122,9 +88,7 @@ async fn should_update_profile( .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? ); Ok(Some(( - item.url - .clone() - .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?, + item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?, item.option.clone(), ))) } @@ -147,7 +111,7 @@ async fn perform_profile_update( let profile_name = profiles_arc .get_name_by_uid(uid) .cloned() - .unwrap_or_else(|| String::from("UnKown Profile")); + .unwrap_or_else(|| String::from("UnKnown Profile")); let mut last_err; @@ -172,11 +136,7 @@ async fn perform_profile_update( match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { Ok(mut item) => { - logging!( - info, - Type::Config, - "[订阅更新] 使用 Clash代理 更新订阅配置成功" - ); + logging!(info, Type::Config, "[订阅更新] 使用 Clash代理 更新订阅配置成功"); profiles_draft_update_item_safe(uid, &mut item).await?; handle::Handle::notice_message("update_with_clash_proxy", profile_name); drop(last_err); @@ -197,11 +157,7 @@ async fn perform_profile_update( match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { Ok(mut item) => { - logging!( - info, - Type::Config, - "[订阅更新] 使用 系统代理 更新订阅配置成功" - ); + logging!(info, Type::Config, "[订阅更新] 使用 系统代理 更新订阅配置成功"); profiles_draft_update_item_safe(uid, &mut item).await?; handle::Handle::notice_message("update_with_clash_proxy", profile_name); drop(last_err); @@ -217,10 +173,7 @@ async fn perform_profile_update( } } - handle::Handle::notice_message( - "update_failed_even_with_clash", - format!("{profile_name} - {last_err}"), - ); + handle::Handle::notice_message("update_failed_even_with_clash", format!("{profile_name} - {last_err}")); Ok(is_current) } @@ -234,9 +187,7 @@ pub async fn update_profile( let url_opt = should_update_profile(uid, ignore_auto_update).await?; let should_refresh = match url_opt { - Some((url, opt)) => { - perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh - } + Some((url, opt)) => perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh, None => auto_refresh, }; diff --git a/clash-verge-rev/src-tauri/src/feat/proxy.rs b/clash-verge-rev/src-tauri/src/feat/proxy.rs index 3f2cadc05e..3b211d9e2d 100644 --- a/clash-verge-rev/src-tauri/src/feat/proxy.rs +++ b/clash-verge-rev/src-tauri/src/feat/proxy.rs @@ -17,11 +17,7 @@ pub async fn toggle_system_proxy() { && auto_close_connection && let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { - logging!( - error, - Type::ProxyMode, - "Failed to close all connections: {err}" - ); + logging!(error, Type::ProxyMode, "Failed to close all connections: {err}"); } let patch_result = super::patch_verge( @@ -86,9 +82,7 @@ pub async fn copy_clash_env() { let env_type = verge_cfg.env_type.as_deref().unwrap_or(default_env); let export_text = match env_type { - "bash" => format!( - "export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}" - ), + "bash" => format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"), "cmd" => format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}"), "powershell" => { format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"") @@ -98,11 +92,7 @@ pub async fn copy_clash_env() { } "fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"), _ => { - logging!( - error, - Type::ProxyMode, - "copy_clash_env: Invalid env type! {env_type}" - ); + logging!(error, Type::ProxyMode, "copy_clash_env: Invalid env type! {env_type}"); return; } }; diff --git a/clash-verge-rev/src-tauri/src/feat/window.rs b/clash-verge-rev/src-tauri/src/feat/window.rs index fde7df86e3..d2d4ba1f40 100644 --- a/clash-verge-rev/src-tauri/src/feat/window.rs +++ b/clash-verge-rev/src-tauri/src/feat/window.rs @@ -45,119 +45,25 @@ pub async fn clean_async() -> bool { // 重置系统代理 let proxy_task = tokio::task::spawn(async { - #[cfg(target_os = "windows")] - { - use sysproxy::{Autoproxy, Sysproxy}; - use winapi::um::winuser::{GetSystemMetrics, SM_SHUTTINGDOWN}; - - // 检查系统代理是否开启 - let sys_proxy_enabled = Config::verge() - .await - .data_arc() - .enable_system_proxy - .unwrap_or(false); - - if !sys_proxy_enabled { - logging!(info, Type::Window, "系统代理未启用,跳过重置"); - return true; - } - - // 检查是否正在关机 - let is_shutting_down = unsafe { GetSystemMetrics(SM_SHUTTINGDOWN) != 0 }; - - if is_shutting_down { - // sysproxy-rs 操作注册表(避免.exe的dll错误) - logging!( - info, - Type::Window, - "检测到正在关机,syspro-rs操作注册表关闭系统代理" - ); - - match Sysproxy::get_system_proxy() { - Ok(mut sysproxy) => { - sysproxy.enable = false; - if let Err(e) = sysproxy.set_system_proxy() { - logging!(warn, Type::Window, "Warning: 关机时关闭系统代理失败: {e}"); - } else { - logging!(info, Type::Window, "系统代理已关闭(通过注册表)"); - } - } - Err(e) => { - logging!(warn, Type::Window, "Warning: 关机时获取代理设置失败: {e}"); - } - } - - // 关闭自动代理配置 - if let Ok(mut autoproxy) = Autoproxy::get_auto_proxy() { - autoproxy.enable = false; - let _ = autoproxy.set_auto_proxy(); - } - - return true; - } - - // 正常退出:使用 sysproxy.exe 重置代理 - logging!(info, Type::Window, "sysproxy.exe重置系统代理"); - - match timeout( - Duration::from_secs(2), - sysopt::Sysopt::global().reset_sysproxy(), - ) - .await - { - Ok(Ok(_)) => { - logging!(info, Type::Window, "系统代理已重置"); - true - } - Ok(Err(e)) => { - logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}"); - true - } - Err(_) => { - logging!( - warn, - Type::Window, - "Warning: 重置系统代理超时,继续退出流程" - ); - true - } - } + let sys_proxy_enabled = Config::verge().await.data_arc().enable_system_proxy.unwrap_or(false); + if !sys_proxy_enabled { + logging!(info, Type::Window, "系统代理未启用,跳过重置"); + return true; } - // 非 Windows 平台:正常重置代理 - #[cfg(not(target_os = "windows"))] - { - let sys_proxy_enabled = Config::verge() - .await - .data_arc() - .enable_system_proxy - .unwrap_or(false); - - if !sys_proxy_enabled { - logging!(info, Type::Window, "系统代理未启用,跳过重置"); - return true; + logging!(info, Type::Window, "开始重置系统代理..."); + match timeout(Duration::from_millis(1500), sysopt::Sysopt::global().reset_sysproxy()).await { + Ok(Ok(_)) => { + logging!(info, Type::Window, "系统代理已重置"); + true } - - logging!(info, Type::Window, "开始重置系统代理..."); - - match timeout( - Duration::from_millis(1500), - sysopt::Sysopt::global().reset_sysproxy(), - ) - .await - { - Ok(Ok(_)) => { - logging!(info, Type::Window, "系统代理已重置"); - true - } - Ok(Err(e)) => { - logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}"); - true - } - Err(_) => { - logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出"); - true - } + Ok(Err(e)) => { + logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}"); + false + } + Err(_) => { + logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出"); + false } } }); @@ -165,20 +71,14 @@ pub async fn clean_async() -> bool { // 关闭 Tun 模式 + 停止核心服务 let core_task = tokio::task::spawn(async { logging!(info, Type::System, "disable tun"); - let tun_enabled = Config::verge() - .await - .data_arc() - .enable_tun_mode - .unwrap_or(false); + let tun_enabled = Config::verge().await.data_arc().enable_tun_mode.unwrap_or(false); if tun_enabled { let disable_tun = serde_json::json!({ "tun": { "enable": false } }); logging!(info, Type::System, "send disable tun request to mihomo"); match timeout( Duration::from_millis(1000), - handle::Handle::mihomo() - .await - .patch_base_config(&disable_tun), + handle::Handle::mihomo().await.patch_base_config(&disable_tun), ) .await { @@ -215,7 +115,7 @@ pub async fn clean_async() -> bool { Type::Window, "Warning: 停止core超时(可能系统正在关机),继续退出" ); - true + false } } }); diff --git a/clash-verge-rev/src-tauri/src/lib.rs b/clash-verge-rev/src-tauri/src/lib.rs index 08e34c4a9e..42d33c9edd 100644 --- a/clash-verge-rev/src-tauri/src/lib.rs +++ b/clash-verge-rev/src-tauri/src/lib.rs @@ -10,10 +10,7 @@ mod feat; mod module; mod process; pub mod utils; -#[cfg(target_os = "linux")] -use crate::utils::linux; -use crate::utils::resolve::init_signal; -use crate::{constants::files, utils::resolve::prioritize_initialization}; +use crate::constants::files; use crate::{ core::handle, process::AsyncHandler, @@ -134,8 +131,7 @@ mod app_init { Ok(()) } - pub fn generate_handlers() - -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { + pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { tauri::generate_handler![ tauri_plugin_clash_verge_sysinfo::commands::get_system_info, tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime, @@ -233,9 +229,6 @@ pub fn run() { let _ = utils::dirs::init_portable_flag(); - #[cfg(target_os = "linux")] - linux::configure_environment(); - let builder = app_init::setup_plugins(tauri::Builder::default()) .setup(|app| { #[allow(clippy::expect_used)] @@ -243,7 +236,7 @@ pub fn run() { .set(app.app_handle().clone()) .expect("failed to set global app handle"); - let _handle = AsyncHandler::block_on(async { prioritize_initialization().await }); + let _handle = resolve::init_work_dir_and_logger(); logging!(info, Type::Setup, "开始应用初始化..."); if let Err(e) = app_init::setup_autostart(app) { @@ -259,7 +252,7 @@ pub fn run() { resolve::resolve_setup_handle(); resolve::resolve_setup_async(); resolve::resolve_setup_sync(); - init_signal(); + resolve::init_signal(); resolve::resolve_done(); logging!(info, Type::Setup, "初始化已启动"); @@ -330,11 +323,7 @@ pub fn run() { pub fn handle_window_focus(focused: bool) { AsyncHandler::spawn(move || async move { - let is_enable_global_hotkey = Config::verge() - .await - .data_arc() - .enable_global_hotkey - .unwrap_or(true); + let is_enable_global_hotkey = Config::verge().await.data_arc().enable_global_hotkey.unwrap_or(true); if focused { #[cfg(target_os = "macos")] @@ -347,7 +336,9 @@ pub fn run() { .register_system_hotkey(SystemHotkey::CmdW) .await; } - let _ = hotkey::Hotkey::global().init(true).await; + if !is_enable_global_hotkey { + let _ = hotkey::Hotkey::global().init(false).await; + } return; } @@ -370,11 +361,7 @@ pub fn run() { AsyncHandler::spawn(move || async move { let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ); let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW); - let is_enable_global_hotkey = Config::verge() - .await - .data_arc() - .enable_global_hotkey - .unwrap_or(true); + let is_enable_global_hotkey = Config::verge().await.data_arc().enable_global_hotkey.unwrap_or(true); if !is_enable_global_hotkey { let _ = hotkey::Hotkey::global().reset(); } @@ -386,27 +373,15 @@ pub fn run() { let context = tauri::test::mock_context(tauri::test::noop_assets()); #[cfg(feature = "clippy")] let app = builder.build(context).unwrap_or_else(|e| { - logging!( - error, - Type::Setup, - "Failed to build Tauri application: {}", - e - ); + logging!(error, Type::Setup, "Failed to build Tauri application: {}", e); std::process::exit(1); }); #[cfg(not(feature = "clippy"))] - let app = builder - .build(tauri::generate_context!()) - .unwrap_or_else(|e| { - logging!( - error, - Type::Setup, - "Failed to build Tauri application: {}", - e - ); - std::process::exit(1); - }); + let app = builder.build(tauri::generate_context!()).unwrap_or_else(|e| { + logging!(error, Type::Setup, "Failed to build Tauri application: {}", e); + std::process::exit(1); + }); app.run(|app_handle, e| match e { tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { @@ -417,8 +392,7 @@ pub fn run() { } #[cfg(target_os = "macos")] tauri::RunEvent::Reopen { - has_visible_windows, - .. + has_visible_windows, .. } => { if core::handle::Handle::global().is_exiting() { return; @@ -427,24 +401,20 @@ pub fn run() { event_handlers::handle_reopen(has_visible_windows).await; }); } - #[cfg(target_os = "macos")] tauri::RunEvent::Exit => AsyncHandler::block_on(async { if !handle::Handle::global().is_exiting() { feat::quit().await; } }), tauri::RunEvent::ExitRequested { api, code, .. } => { - AsyncHandler::block_on(async { - let _ = handle::Handle::mihomo() - .await - .clear_all_ws_connections() - .await; - }); - if core::handle::Handle::global().is_exiting() { return; } + AsyncHandler::block_on(async { + let _ = handle::Handle::mihomo().await.clear_all_ws_connections().await; + }); + if code.is_none() { api.prevent_exit(); } diff --git a/clash-verge-rev/src-tauri/src/module/auto_backup.rs b/clash-verge-rev/src-tauri/src/module/auto_backup.rs index 6c6b2dbc74..67e61530cb 100644 --- a/clash-verge-rev/src-tauri/src/module/auto_backup.rs +++ b/clash-verge-rev/src-tauri/src/module/auto_backup.rs @@ -214,27 +214,14 @@ impl AutoBackupManager { return Ok(()); } - let file_name = - create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()) - .await?; - self.last_backup - .store(Local::now().timestamp(), Ordering::Release); + let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()).await?; + self.last_backup.store(Local::now().timestamp(), Ordering::Release); if let Err(err) = cleanup_auto_backups().await { - logging!( - warn, - Type::Backup, - "Failed to cleanup old auto backups: {err:#?}" - ); + logging!(warn, Type::Backup, "Failed to cleanup old auto backups: {err:#?}"); } - logging!( - info, - Type::Backup, - "Auto backup created ({:?}): {}", - trigger, - file_name - ); + logging!(info, Type::Backup, "Auto backup created ({:?}): {}", trigger, file_name); Ok(()) } @@ -273,11 +260,7 @@ async fn cleanup_auto_backups() -> Result<()> { let mut entries = match fs::read_dir(&backup_dir).await { Ok(dir) => dir, Err(err) => { - logging!( - warn, - Type::Backup, - "Failed to read backup directory: {err:#?}" - ); + logging!(warn, Type::Backup, "Failed to read backup directory: {err:#?}"); return Ok(()); } }; diff --git a/clash-verge-rev/src-tauri/src/module/lightweight.rs b/clash-verge-rev/src-tauri/src/module/lightweight.rs index e91837426c..04f87230ae 100644 --- a/clash-verge-rev/src-tauri/src/module/lightweight.rs +++ b/clash-verge-rev/src-tauri/src/module/lightweight.rs @@ -51,12 +51,7 @@ fn get_state() -> LightweightState { #[inline] fn try_transition(from: LightweightState, to: LightweightState) -> bool { LIGHTWEIGHT_STATE - .compare_exchange( - from.as_u8(), - to.as_u8(), - Ordering::AcqRel, - Ordering::Relaxed, - ) + .compare_exchange(from.as_u8(), to.as_u8(), Ordering::AcqRel, Ordering::Relaxed) .is_ok() } @@ -83,10 +78,7 @@ async fn refresh_lightweight_tray_state() { pub async fn auto_lightweight_boot() -> Result<()> { let verge_config = Config::verge().await; - let is_enable_auto = verge_config - .data_arc() - .enable_auto_light_weight_mode - .unwrap_or(false); + let is_enable_auto = verge_config.data_arc().enable_auto_light_weight_mode.unwrap_or(false); let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false); if is_enable_auto { enable_auto_light_weight_mode().await; @@ -182,11 +174,7 @@ fn setup_webview_focus_listener() { if let Some(window) = handle::Handle::get_window() { let handler_id = window.listen("tauri://focus", move |_event| { logging_error!(Type::Lightweight, cancel_light_weight_timer()); - logging!( - debug, - Type::Lightweight, - "监听到窗口获得焦点,取消轻量模式计时" - ); + logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时"); }); WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release); } @@ -207,11 +195,7 @@ async fn setup_light_weight_timer() -> Result<()> { return Err(e).context("failed to initialize timer"); } - let once_by_minutes = Config::verge() - .await - .data_arc() - .auto_light_weight_minutes - .unwrap_or(10); + let once_by_minutes = Config::verge().await.data_arc().auto_light_weight_minutes.unwrap_or(10); { let timer_map = Timer::global().timer_map.read(); @@ -239,9 +223,7 @@ async fn setup_light_weight_timer() -> Result<()> { { let delay_timer = Timer::global().delay_timer.write(); - delay_timer - .add_task(task) - .context("failed to add timer task")?; + delay_timer.add_task(task).context("failed to add timer task")?; } { @@ -265,10 +247,7 @@ async fn setup_light_weight_timer() -> Result<()> { } fn cancel_light_weight_timer() -> Result<()> { - let value = Timer::global() - .timer_map - .write() - .remove(LIGHT_WEIGHT_TASK_UID); + let value = Timer::global().timer_map.write().remove(LIGHT_WEIGHT_TASK_UID); if let Some(task) = value { Timer::global() .delay_timer diff --git a/clash-verge-rev/src-tauri/src/process/async_handler.rs b/clash-verge-rev/src-tauri/src/process/async_handler.rs index 8c79662077..f277ec6aae 100644 --- a/clash-verge-rev/src-tauri/src/process/async_handler.rs +++ b/clash-verge-rev/src-tauri/src/process/async_handler.rs @@ -62,12 +62,7 @@ impl AsyncHandler { let location = Location::caller(); let type_str = type_name::(); let size_str = format!("{} bytes", size); - let loc_str = format!( - "{}:{}:{}", - location.file(), - location.line(), - location.column() - ); + let loc_str = format!("{}:{}:{}", location.file(), location.line(), location.column()); println!( "┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐" diff --git a/clash-verge-rev/src-tauri/src/utils/autostart.rs b/clash-verge-rev/src-tauri/src/utils/autostart.rs index 3de6d988ea..65128e55ee 100644 --- a/clash-verge-rev/src-tauri/src/utils/autostart.rs +++ b/clash-verge-rev/src-tauri/src/utils/autostart.rs @@ -28,8 +28,7 @@ pub fn get_startup_dir() -> Result { /// 获取当前可执行文件路径 #[cfg(target_os = "windows")] pub fn get_exe_path() -> Result { - let exe_path = - std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?; + let exe_path = std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?; Ok(exe_path) } diff --git a/clash-verge-rev/src-tauri/src/utils/dirs.rs b/clash-verge-rev/src-tauri/src/utils/dirs.rs index 3bda8e60c6..2e986e1a3c 100644 --- a/clash-verge-rev/src-tauri/src/utils/dirs.rs +++ b/clash-verge-rev/src-tauri/src/utils/dirs.rs @@ -60,11 +60,7 @@ pub fn app_home_dir() -> Result { match app_handle.path().data_dir() { Ok(dir) => Ok(dir.join(APP_ID)), Err(e) => { - logging!( - error, - Type::File, - "Failed to get the app home directory: {e}" - ); + logging!(error, Type::File, "Failed to get the app home directory: {e}"); Err(anyhow::anyhow!("Failed to get the app homedirectory")) } } @@ -78,11 +74,7 @@ pub fn app_resources_dir() -> Result { match app_handle.path().resource_dir() { Ok(dir) => Ok(dir.join("resources")), Err(e) => { - logging!( - error, - Type::File, - "Failed to get the resource directory: {e}" - ); + logging!(error, Type::File, "Failed to get the resource directory: {e}"); Err(anyhow::anyhow!("Failed to get the resource directory")) } } @@ -110,15 +102,11 @@ pub fn find_target_icons(target: &str) -> Result> { let ext_matches = path .extension() .and_then(|e| e.to_str()) - .is_some_and(|ext| { - ext.eq_ignore_ascii_case("ico") || ext.eq_ignore_ascii_case("png") - }); + .is_some_and(|ext| ext.eq_ignore_ascii_case("ico") || ext.eq_ignore_ascii_case("png")); prefix_matches && ext_matches }); - icon_path - .map(|path| path_to_str(&path).map(|s| s.into())) - .transpose() + icon_path.map(|path| path_to_str(&path).map(|s| s.into())).transpose() } /// logs dir @@ -179,9 +167,7 @@ pub fn service_log_dir() -> Result { pub fn clash_latest_log() -> Result { match *CoreManager::global().get_running_mode() { RunningMode::Service => Ok(service_log_dir()?.join("service_latest.log")), - RunningMode::Sidecar | RunningMode::NotRunning => { - Ok(sidecar_log_dir()?.join("sidecar_latest.log")) - } + RunningMode::Sidecar | RunningMode::NotRunning => Ok(sidecar_log_dir()?.join("sidecar_latest.log")), } } @@ -207,12 +193,10 @@ pub fn get_encryption_key() -> Result> { // Ensure directory exists if let Some(parent) = key_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| anyhow::anyhow!("Failed to create key directory: {}", e))?; + fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Failed to create key directory: {}", e))?; } // Save key - fs::write(&key_path, &key) - .map_err(|e| anyhow::anyhow!("Failed to save encryption key: {}", e))?; + fs::write(&key_path, &key).map_err(|e| anyhow::anyhow!("Failed to save encryption key: {}", e))?; Ok(key) } } @@ -228,11 +212,7 @@ pub fn ensure_mihomo_safe_dir() -> Option { if home_config.exists() || fs::create_dir_all(&home_config).is_ok() { Some(home_config) } else { - logging!( - error, - Type::File, - "Failed to create safe directory: {home_config:?}" - ); + logging!(error, Type::File, "Failed to create safe directory: {home_config:?}"); None } }) diff --git a/clash-verge-rev/src-tauri/src/utils/help.rs b/clash-verge-rev/src-tauri/src/utils/help.rs index 1206ff57a1..d676b31c72 100644 --- a/clash-verge-rev/src-tauri/src/utils/help.rs +++ b/clash-verge-rev/src-tauri/src/utils/help.rs @@ -35,19 +35,14 @@ pub async fn read_mapping(path: &PathBuf) -> Result { Ok(val .as_mapping() - .ok_or_else(|| { - anyhow!("failed to transform to yaml mapping \"{}\"", path.display()) - })? + .ok_or_else(|| anyhow!("failed to transform to yaml mapping \"{}\"", path.display()))? .to_owned()) } Err(err) => { let error_msg = format!("YAML syntax error in {}: {}", path.display(), err); logging!(error, Type::Config, "{}", error_msg); - crate::core::handle::Handle::notice_message( - "config_validate::yaml_syntax_error", - &error_msg, - ); + crate::core::handle::Handle::notice_message("config_validate::yaml_syntax_error", &error_msg); bail!("YAML syntax error: {}", err) } @@ -61,11 +56,7 @@ pub async fn read_seq_map(path: &PathBuf) -> Result { /// save the data to the file /// can set `prefix` string to add some comments -pub async fn save_yaml( - path: &PathBuf, - data: &T, - prefix: Option<&str>, -) -> Result<()> { +pub async fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?; let yaml_str = match prefix { @@ -82,10 +73,9 @@ pub async fn save_yaml( } const ALPHABET: [char; 62] = [ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', - 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', - 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', - 'V', 'W', 'X', 'Y', 'Z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ]; /// generate the uid diff --git a/clash-verge-rev/src-tauri/src/utils/i18n.rs b/clash-verge-rev/src-tauri/src/utils/i18n.rs index 1bd99319ec..dc8c9bf31b 100644 --- a/clash-verge-rev/src-tauri/src/utils/i18n.rs +++ b/clash-verge-rev/src-tauri/src/utils/i18n.rs @@ -50,11 +50,9 @@ fn resolve_supported_language(language: &str) -> Option { let supported = supported_languages_internal(); - candidates.into_iter().find(|candidate| { - supported - .iter() - .any(|&lang| lang.eq_ignore_ascii_case(candidate)) - }) + candidates + .into_iter() + .find(|candidate| supported.iter().any(|&lang| lang.eq_ignore_ascii_case(candidate))) } fn system_language() -> String { @@ -72,8 +70,7 @@ pub fn get_supported_languages() -> Vec { } pub fn set_locale(language: &str) { - let lang = - resolve_supported_language(language).unwrap_or_else(|| fallback_language().to_string()); + let lang = resolve_supported_language(language).unwrap_or_else(|| fallback_language().to_string()); rust_i18n::set_locale(&lang); } diff --git a/clash-verge-rev/src-tauri/src/utils/init.rs b/clash-verge-rev/src-tauri/src/utils/init.rs index eff1a9a58d..7671832548 100644 --- a/clash-verge-rev/src-tauri/src/utils/init.rs +++ b/clash-verge-rev/src-tauri/src/utils/init.rs @@ -12,7 +12,7 @@ use crate::{ }; use anyhow::Result; use chrono::{Local, TimeZone as _}; -#[cfg(not(feature = "tauri-dev"))] +#[cfg(all(not(feature = "tauri-dev"), not(feature = "tracing-full")))] use clash_verge_logging::NoModuleFilter; use clash_verge_logging::Type; use clash_verge_service_ipc::WriterConfig; @@ -65,7 +65,7 @@ pub async fn init_logger() -> Result { }, Cleanup::KeepLogFiles(log_max_count), ); - #[cfg(not(feature = "tracing"))] + #[cfg(all(not(feature = "tracing"), not(feature = "tracing-full")))] let logger = logger.filter(Box::new(NoModuleFilter(&[ "wry", "tauri", @@ -223,14 +223,8 @@ async fn init_dns_config() -> Result<()> { ("enable".into(), Value::Bool(true)), ("listen".into(), Value::String(":53".into())), ("enhanced-mode".into(), Value::String("fake-ip".into())), - ( - "fake-ip-range".into(), - Value::String("198.18.0.1/16".into()), - ), - ( - "fake-ip-filter-mode".into(), - Value::String("blacklist".into()), - ), + ("fake-ip-range".into(), Value::String("198.18.0.1/16".into())), + ("fake-ip-filter-mode".into(), Value::String("blacklist".into())), ("prefer-h3".into(), Value::Bool(false)), ("respect-rules".into(), Value::Bool(false)), ("use-hosts".into(), Value::Bool(false)), @@ -310,10 +304,7 @@ async fn init_dns_config() -> Result<()> { // 获取默认DNS和host配置 let default_dns_config = serde_yaml_ng::Mapping::from_iter([ ("dns".into(), Value::Mapping(dns_config)), - ( - "hosts".into(), - Value::Mapping(serde_yaml_ng::Mapping::new()), - ), + ("hosts".into(), Value::Mapping(serde_yaml_ng::Mapping::new())), ]); // 检查DNS配置文件是否存在 @@ -322,12 +313,7 @@ async fn init_dns_config() -> Result<()> { if !dns_path.exists() { logging!(info, Type::Setup, "Creating default DNS config file"); - help::save_yaml( - &dns_path, - &default_dns_config, - Some("# Clash Verge DNS Config"), - ) - .await?; + help::save_yaml(&dns_path, &default_dns_config, Some("# Clash Verge DNS Config")).await?; } Ok(()) @@ -343,9 +329,9 @@ async fn ensure_directories() -> Result<()> { for (name, dir) in directories { if !dir.exists() { - fs::create_dir_all(&dir).await.map_err(|e| { - anyhow::anyhow!("Failed to create {} directory {:?}: {}", name, dir, e) - })?; + fs::create_dir_all(&dir) + .await + .map_err(|e| anyhow::anyhow!("Failed to create {} directory {:?}: {}", name, dir, e))?; logging!(info, Type::Setup, "Created {} directory: {:?}", name, dir); } } @@ -535,10 +521,7 @@ pub async fn startup_script() -> Result<()> { } else if script_path.ends_with(".ps1") || script_path.ends_with(".bat") { "powershell" } else { - return Err(anyhow::anyhow!( - "unsupported script extension: {}", - script_path - )); + return Err(anyhow::anyhow!("unsupported script extension: {}", script_path)); }; let script_dir = PathBuf::from(script_path.as_str()); diff --git a/clash-verge-rev/src-tauri/src/utils/linux.rs b/clash-verge-rev/src-tauri/src/utils/linux.rs index de577cabc2..f0b03de190 100644 --- a/clash-verge-rev/src-tauri/src/utils/linux.rs +++ b/clash-verge-rev/src-tauri/src/utils/linux.rs @@ -1,479 +1,9 @@ use anyhow::Result; -use clash_verge_logging::{Type, logging}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::env; use std::fs; use std::path::PathBuf; -const DRM_PATH: &str = "/sys/class/drm"; -const INTEL_VENDOR_ID: &str = "0x8086"; -const NVIDIA_VENDOR_ID: &str = "0x10de"; -const NVIDIA_VERSION_PATH: &str = "/proc/driver/nvidia/version"; - -#[derive(Debug, Default, Clone, Copy)] -struct IntelGpuDetection { - has_intel: bool, - intel_is_primary: bool, - inconclusive: bool, -} - -impl IntelGpuDetection { - const fn should_disable_dmabuf(&self) -> bool { - self.intel_is_primary || self.inconclusive - } -} - -#[derive(Debug, Default, Clone)] -struct NvidiaGpuDetection { - has_nvidia: bool, - nvidia_is_primary: bool, - missing_boot_vga: bool, - open_kernel_module: bool, - driver_summary: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum NvidiaDmabufDisableReason { - PrimaryOpenKernelModule, - MissingBootVga, - PreferNativeWayland, -} - -impl NvidiaGpuDetection { - const fn disable_reason(&self, session: &SessionEnv) -> Option { - if !session.is_wayland { - return None; - } - - if !self.has_nvidia { - return None; - } - - if !self.open_kernel_module { - return None; - } - - if self.nvidia_is_primary { - return Some(NvidiaDmabufDisableReason::PrimaryOpenKernelModule); - } - - if self.missing_boot_vga { - return Some(NvidiaDmabufDisableReason::MissingBootVga); - } - - if session.prefer_native_wayland { - return Some(NvidiaDmabufDisableReason::PreferNativeWayland); - } - - None - } -} - -#[derive(Debug)] -struct SessionEnv { - is_kde_plasma: bool, - is_wayland: bool, - prefer_native_wayland: bool, - compositor_label: String, -} - -impl SessionEnv { - fn gather() -> Self { - let desktop_env = env::var("XDG_CURRENT_DESKTOP") - .unwrap_or_default() - .to_uppercase(); - let session_desktop = env::var("XDG_SESSION_DESKTOP") - .unwrap_or_default() - .to_uppercase(); - let desktop_session = env::var("DESKTOP_SESSION") - .unwrap_or_default() - .to_uppercase(); - - let is_kde_plasma = desktop_env.contains("KDE") - || session_desktop.contains("KDE") - || desktop_session.contains("KDE") - || desktop_env.contains("PLASMA") - || session_desktop.contains("PLASMA") - || desktop_session.contains("PLASMA"); - let is_hyprland = desktop_env.contains("HYPR") - || session_desktop.contains("HYPR") - || desktop_session.contains("HYPR"); - let is_wayland = env::var("XDG_SESSION_TYPE") - .map(|value| value.eq_ignore_ascii_case("wayland")) - .unwrap_or(false) - || env::var("WAYLAND_DISPLAY").is_ok(); - let prefer_native_wayland = is_wayland && (is_kde_plasma || is_hyprland); - let compositor_label = if is_hyprland { - String::from("Hyprland") - } else if is_kde_plasma { - String::from("KDE Plasma") - } else { - String::from("Wayland compositor") - }; - - Self { - is_kde_plasma, - is_wayland, - prefer_native_wayland, - compositor_label, - } - } -} - -#[derive(Debug)] -struct DmabufOverrides { - user_preference: Option, - dmabuf_override: Option, -} - -impl DmabufOverrides { - fn gather() -> Self { - let user_preference = env::var("CLASH_VERGE_DMABUF").ok().and_then(|value| { - match value.trim().to_ascii_lowercase().as_str() { - "1" | "true" | "enable" | "on" => Some(true), - "0" | "false" | "disable" | "off" => Some(false), - _ => None, - } - }); - let dmabuf_override = env::var("WEBKIT_DISABLE_DMABUF_RENDERER").ok(); - - Self { - user_preference, - dmabuf_override, - } - } - - const fn has_env_override(&self) -> bool { - self.dmabuf_override.is_some() - } - - const fn should_override_env(&self, decision: &DmabufDecision) -> bool { - if self.user_preference.is_some() { - return true; - } - - if decision.enable_dmabuf { - return true; - } - - !self.has_env_override() - } -} - -#[derive(Debug)] -struct DmabufDecision { - enable_dmabuf: bool, - force_x11_backend: bool, - warn: bool, - message: Option, -} - -impl DmabufDecision { - fn resolve( - session: &SessionEnv, - overrides: &DmabufOverrides, - intel_gpu: IntelGpuDetection, - nvidia_gpu: &NvidiaGpuDetection, - ) -> Self { - let mut decision = Self { - enable_dmabuf: true, - force_x11_backend: false, - warn: false, - message: None, - }; - - match overrides.user_preference { - Some(true) => { - decision.enable_dmabuf = true; - decision.message = - Some("CLASH_VERGE_DMABUF=1: 强制启用 WebKit DMABUF 渲染。".into()); - } - Some(false) => { - decision.enable_dmabuf = false; - decision.message = - Some("CLASH_VERGE_DMABUF=0: 强制禁用 WebKit DMABUF 渲染。".into()); - if session.is_wayland && !session.prefer_native_wayland { - decision.force_x11_backend = true; - } - } - None => { - if overrides.has_env_override() { - if overrides.dmabuf_override.as_deref() == Some("1") { - decision.enable_dmabuf = false; - decision.message = Some( - "检测到 WEBKIT_DISABLE_DMABUF_RENDERER=1,沿用用户的软件渲染配置。" - .into(), - ); - if session.is_wayland && !session.prefer_native_wayland { - decision.force_x11_backend = true; - } - } else { - decision.enable_dmabuf = true; - let value = overrides.dmabuf_override.clone().unwrap_or_default(); - decision.message = Some(format!( - "检测到 WEBKIT_DISABLE_DMABUF_RENDERER={},沿用用户配置。", - value - )); - } - } else if let Some(reason) = nvidia_gpu.disable_reason(session) { - decision.enable_dmabuf = false; - decision.warn = true; - if session.is_wayland && !session.prefer_native_wayland { - decision.force_x11_backend = true; - } - let summary = nvidia_gpu - .driver_summary - .as_deref() - .and_then(|line| { - extract_nvidia_driver_version(line) - .map(|version| format!("NVIDIA Open Kernel Module {}", version)) - }) - .unwrap_or_else(|| String::from("NVIDIA Open Kernel Module")); - let message = match reason { - NvidiaDmabufDisableReason::PrimaryOpenKernelModule => format!( - "Wayland 会话检测到 {}:禁用 WebKit DMABUF 渲染以规避协议错误。", - summary - ), - NvidiaDmabufDisableReason::MissingBootVga => format!( - "Wayland 会话检测到 {},但缺少 boot_vga 信息:预防性禁用 WebKit DMABUF。", - summary - ), - NvidiaDmabufDisableReason::PreferNativeWayland => format!( - "Wayland ({}) + {}:检测到 NVIDIA Open Kernel Module 在辅 GPU 上运行,预防性禁用 WebKit DMABUF。", - session.compositor_label, summary - ), - }; - decision.message = Some(message); - } else if session.prefer_native_wayland && !intel_gpu.should_disable_dmabuf() { - decision.enable_dmabuf = true; - decision.message = Some(format!( - "Wayland + {} detected: 使用原生 DMABUF 渲染。", - session.compositor_label - )); - } else { - decision.enable_dmabuf = false; - if session.is_wayland && !session.prefer_native_wayland { - decision.force_x11_backend = true; - } - - if intel_gpu.should_disable_dmabuf() && session.is_wayland { - decision.warn = true; - if intel_gpu.inconclusive { - decision.message = Some("Wayland 上检测到 Intel GPU,但缺少 boot_vga 信息:预防性禁用 WebKit DMABUF,若确认非主 GPU 可通过 CLASH_VERGE_DMABUF=1 覆盖。".into()); - } else { - decision.message = Some( - "Wayland 上检测到 Intel 主 GPU (0x8086):禁用 WebKit DMABUF 以避免帧缓冲失败。".into(), - ); - } - } else if session.is_wayland { - decision.message = Some( - "Wayland 会话未匹配受支持的合成器:禁用 WebKit DMABUF 渲染。".into(), - ); - } else { - decision.message = - Some("禁用 WebKit DMABUF 渲染以获得更稳定的输出。".into()); - } - } - } - } - - decision - } -} - -fn detect_intel_gpu() -> IntelGpuDetection { - let Ok(entries) = fs::read_dir(DRM_PATH) else { - return IntelGpuDetection::default(); - }; - - let mut detection = IntelGpuDetection::default(); - let mut seen_devices: HashSet = HashSet::new(); - let mut missing_boot_vga = false; - - for entry in entries.flatten() { - let name = entry.file_name(); - let name = name.to_string_lossy(); - - if !(name.starts_with("renderD") || name.starts_with("card")) { - continue; - } - - let device_path = entry.path().join("device"); - let device_key = fs::canonicalize(&device_path).unwrap_or(device_path); - - if !seen_devices.insert(device_key.clone()) { - continue; - } - - let vendor_path = device_key.join("vendor"); - let Ok(vendor) = fs::read_to_string(&vendor_path) else { - continue; - }; - - if !vendor.trim().eq_ignore_ascii_case(INTEL_VENDOR_ID) { - continue; - } - - detection.has_intel = true; - - let boot_vga_path = device_key.join("boot_vga"); - match fs::read_to_string(&boot_vga_path) { - Ok(flag) => { - if flag.trim() == "1" { - detection.intel_is_primary = true; - } - } - Err(_) => { - missing_boot_vga = true; - } - } - } - - if detection.has_intel && !detection.intel_is_primary && missing_boot_vga { - detection.inconclusive = true; - } - - detection -} - -fn detect_nvidia_gpu() -> NvidiaGpuDetection { - let mut detection = NvidiaGpuDetection::default(); - let entries = match fs::read_dir(DRM_PATH) { - Ok(entries) => entries, - Err(err) => { - logging!( - info, - Type::Setup, - "无法读取 DRM 设备目录 {}({}),尝试通过 NVIDIA 驱动摘要进行降级检测。", - DRM_PATH, - err - ); - detection.driver_summary = read_nvidia_driver_summary(); - if let Some(summary) = detection.driver_summary.as_ref() { - detection.open_kernel_module = summary_indicates_open_kernel_module(summary); - detection.has_nvidia = true; - detection.missing_boot_vga = true; - } else { - logging!( - info, - Type::Setup, - "降级检测失败:未能读取 NVIDIA 驱动摘要,保留 WebKit DMABUF。" - ); - } - return detection; - } - }; - - let mut seen_devices: HashSet = HashSet::new(); - - for entry in entries.flatten() { - let name = entry.file_name(); - let name = name.to_string_lossy(); - - if !(name.starts_with("renderD") || name.starts_with("card")) { - continue; - } - - let device_path = entry.path().join("device"); - let device_key = fs::canonicalize(&device_path).unwrap_or(device_path); - - if !seen_devices.insert(device_key.clone()) { - continue; - } - - let vendor_path = device_key.join("vendor"); - let Ok(vendor) = fs::read_to_string(&vendor_path) else { - continue; - }; - - if !vendor.trim().eq_ignore_ascii_case(NVIDIA_VENDOR_ID) { - continue; - } - - detection.has_nvidia = true; - - let boot_vga_path = device_key.join("boot_vga"); - match fs::read_to_string(&boot_vga_path) { - Ok(flag) => { - if flag.trim() == "1" { - detection.nvidia_is_primary = true; - } - } - Err(_) => { - detection.missing_boot_vga = true; - } - } - } - - if detection.has_nvidia { - detection.driver_summary = read_nvidia_driver_summary(); - match detection.driver_summary.as_ref() { - Some(summary) => { - detection.open_kernel_module = summary_indicates_open_kernel_module(summary); - } - None => { - logging!( - info, - Type::Setup, - "检测到 NVIDIA 设备,但无法读取 {},默认视为未启用开源内核模块。", - NVIDIA_VERSION_PATH - ); - } - } - } - - detection -} - -fn read_nvidia_driver_summary() -> Option { - match fs::read_to_string(NVIDIA_VERSION_PATH) { - Ok(content) => content - .lines() - .next() - .map(|line| line.trim().to_string()) - .filter(|line| !line.is_empty()), - Err(err) => { - logging!( - info, - Type::Setup, - "读取 {} 失败:{}", - NVIDIA_VERSION_PATH, - err - ); - None - } - } -} - -fn summary_indicates_open_kernel_module(summary: &str) -> bool { - let normalized = summary.to_ascii_lowercase(); - const PATTERNS: [&str; 4] = [ - "open kernel module", - "open kernel modules", - "open gpu kernel module", - "open gpu kernel modules", - ]; - - let is_open = PATTERNS.iter().any(|pattern| normalized.contains(pattern)); - - if !is_open && normalized.contains("open") { - logging!( - info, - Type::Setup, - "检测到 NVIDIA 驱动摘要包含 open 关键字但未匹配已知开源模块格式:{}", - summary - ); - } - - is_open -} - -fn extract_nvidia_driver_version(summary: &str) -> Option<&str> { - summary - .split_whitespace() - .find(|token| token.chars().all(|c| c.is_ascii_digit() || c == '.')) -} - pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<()> { let Some(path) = mimeapps_list_path() else { return Ok(()); @@ -639,10 +169,7 @@ fn flush_section( if let Some(&index) = seen.get(scheme) { let existing_line = &mut processed[index]; - let existing_prefix: String = existing_line - .chars() - .take_while(|c| c.is_whitespace()) - .collect(); + let existing_prefix: String = existing_line.chars().take_while(|c| c.is_whitespace()).collect(); let Some((_, existing_raw_value)) = existing_line.trim().split_once('=') else { processed.push(line); continue; @@ -695,10 +222,7 @@ fn flush_section( *changed = true; } - let prefix = line - .chars() - .take_while(|c| c.is_whitespace()) - .collect::(); + let prefix = line.chars().take_while(|c| c.is_whitespace()).collect::(); let mut new_line = format!("{prefix}x-scheme-handler/{scheme}="); new_line.push_str(&values.join(";")); new_line.push(';'); @@ -716,10 +240,7 @@ fn flush_section( processed.push(line); } - let ensure_all = matches!( - kind, - SectionKind::DefaultApplications | SectionKind::AddedAssociations - ); + let ensure_all = matches!(kind, SectionKind::DefaultApplications | SectionKind::AddedAssociations); if ensure_all { for &scheme in schemes { @@ -740,52 +261,3 @@ fn match_scheme<'a>(key: &str, schemes: &'a [&str]) -> Option<&'a str> { schemes.iter().copied().find(|candidate| *candidate == key) } - -pub fn configure_environment() { - let session = SessionEnv::gather(); - let overrides = DmabufOverrides::gather(); - let intel_gpu = detect_intel_gpu(); - let nvidia_gpu = detect_nvidia_gpu(); - let decision = DmabufDecision::resolve(&session, &overrides, intel_gpu, &nvidia_gpu); - - if overrides.should_override_env(&decision) { - unsafe { - if decision.enable_dmabuf { - env::remove_var("WEBKIT_DISABLE_DMABUF_RENDERER"); - } else { - env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); - } - } - } - - if let Some(message) = decision.message { - if decision.warn { - logging!(warn, Type::Setup, "{}", message); - } else { - logging!(info, Type::Setup, "{}", message); - } - } - - if decision.force_x11_backend { - unsafe { - env::set_var("GDK_BACKEND", "x11"); - env::remove_var("WAYLAND_DISPLAY"); - } - logging!( - info, - Type::Setup, - "Wayland detected: Forcing X11 backend for WebKit stability." - ); - } - - if session.is_kde_plasma { - unsafe { - env::set_var("GTK_CSD", "0"); - } - logging!( - info, - Type::Setup, - "KDE/Plasma detected: Disabled GTK CSD for better titlebar stability." - ); - } -} diff --git a/clash-verge-rev/src-tauri/src/utils/network.rs b/clash-verge-rev/src-tauri/src/utils/network.rs index c1dbc85763..837da05bb2 100644 --- a/clash-verge-rev/src-tauri/src/utils/network.rs +++ b/clash-verge-rev/src-tauri/src/utils/network.rs @@ -20,11 +20,7 @@ pub struct HttpResponse { impl HttpResponse { pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self { - Self { - status, - headers, - body, - } + Self { status, headers, body } } pub const fn status(&self) -> StatusCode { @@ -208,10 +204,7 @@ impl NetworkManager { { let auth_str = format!("{}:{}", parsed.username(), pass); let encoded = general_purpose::STANDARD.encode(auth_str); - extra_headers.insert( - "Authorization", - HeaderValue::from_str(&format!("Basic {}", encoded))?, - ); + extra_headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", encoded))?); } let clean_url = { @@ -235,8 +228,7 @@ impl NetworkManager { let response = match request_builder.send().await { Ok(resp) => resp, Err(e) => { - self.record_connection_error(&format!("Request failed: {}", e)) - .await; + self.record_connection_error(&format!("Request failed: {}", e)).await; return Err(anyhow::anyhow!("Request failed: {}", e)); } }; diff --git a/clash-verge-rev/src-tauri/src/utils/notification.rs b/clash-verge-rev/src-tauri/src/utils/notification.rs index 1308be6b6b..e35eb0c225 100644 --- a/clash-verge-rev/src-tauri/src/utils/notification.rs +++ b/clash-verge-rev/src-tauri/src/utils/notification.rs @@ -17,13 +17,7 @@ pub enum NotificationEvent<'a> { fn notify(title: &str, body: &str) { let app_handle = handle::Handle::app_handle(); - app_handle - .notification() - .builder() - .title(title) - .body(body) - .show() - .ok(); + app_handle.notification().builder().title(title).body(body).show().ok(); } pub async fn notify_event<'a>(event: NotificationEvent<'a>) { diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs b/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs index 12dd86bd8c..64005d0472 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs @@ -9,12 +9,7 @@ pub async fn set_public_dns(dns_server: String) { let resource_dir = match dirs::app_resources_dir() { Ok(dir) => dir, Err(e) => { - logging!( - error, - Type::Config, - "Failed to get resource directory: {}", - e - ); + logging!(error, Type::Config, "Failed to get resource directory: {}", e); return; } }; @@ -55,12 +50,7 @@ pub async fn restore_public_dns() { let resource_dir = match dirs::app_resources_dir() { Ok(dir) => dir, Err(e) => { - logging!( - error, - Type::Config, - "Failed to get resource directory: {}", - e - ); + logging!(error, Type::Config, "Failed to get resource directory: {}", e); return; } }; diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs b/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs index 639072e258..042d5d56e1 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs @@ -28,19 +28,21 @@ pub mod window_script; static RESOLVE_DONE: AtomicBool = AtomicBool::new(false); -pub async fn prioritize_initialization() -> Option { - init_work_config().await; - init_resources().await; +pub fn init_work_dir_and_logger() -> Option { + AsyncHandler::block_on(async { + init_work_config().await; + init_resources().await; - #[cfg(not(feature = "tauri-dev"))] - { - logging!(info, Type::Setup, "Initializing logger"); - init::init_logger().await.ok() - } - #[cfg(feature = "tauri-dev")] - { - None - } + #[cfg(not(feature = "tauri-dev"))] + { + logging!(info, Type::Setup, "Initializing logger"); + init::init_logger().await.ok() + } + #[cfg(feature = "tauri-dev")] + { + None + } + }) } pub fn resolve_setup_handle() { @@ -56,15 +58,9 @@ pub fn resolve_setup_sync() { pub fn resolve_setup_async() { AsyncHandler::spawn(|| async { - logging!( - info, - Type::ClashVergeRev, - "Version: {}", - env!("CARGO_PKG_VERSION") - ); - - futures::join!(init_work_config(), init_resources(), init_startup_script()); + logging!(info, Type::ClashVergeRev, "Version: {}", env!("CARGO_PKG_VERSION")); + init_startup_script().await; init_verge_config().await; Config::verify_config_initialization().await; init_window().await; @@ -135,7 +131,9 @@ pub(super) async fn init_timer() { } pub(super) async fn init_hotkey() { - logging_error!(Type::Setup, Hotkey::global().init(false).await); + // if hotkey is not use by global, skip init it + let skip_register_hotkeys = !Config::verge().await.latest_arc().enable_global_hotkey.unwrap_or(true); + logging_error!(Type::Setup, Hotkey::global().init(skip_register_hotkeys).await); } pub(super) async fn init_auto_lightweight_boot() { @@ -148,11 +146,7 @@ pub(super) async fn init_auto_backup() { pub fn init_signal() { logging!(info, Type::Setup, "Initializing signal handlers..."); - clash_verge_signal::register( - #[cfg(windows)] - handle::Handle::app_handle(), - feat::quit, - ); + clash_verge_signal::register(feat::quit); } pub async fn init_work_config() { @@ -185,10 +179,7 @@ pub(super) async fn init_core_manager() { } pub(super) async fn init_system_proxy() { - logging_error!( - Type::Setup, - sysopt::Sysopt::global().update_sysproxy().await - ); + logging_error!(Type::Setup, sysopt::Sysopt::global().update_sysproxy().await); } pub(super) async fn init_system_proxy_guard() { @@ -200,11 +191,7 @@ pub(super) async fn refresh_tray_menu() { } pub(super) async fn init_window() { - let is_silent_start = Config::verge() - .await - .data_arc() - .enable_silent_start - .unwrap_or(false); + let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false); #[cfg(target_os = "macos")] if is_silent_start { use crate::core::handle::Handle; diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs b/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs index 6fa02230e4..47aa3a046b 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs @@ -30,50 +30,39 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> { } }; - let (url_param, name) = - if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { - let name_owned: Option = link_parsed - .query_pairs() - .find(|(key, _)| key == "name") - .map(|(_, value)| value.into_owned().into()); + let (url_param, name) = if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { + let name_owned: Option = link_parsed + .query_pairs() + .find(|(key, _)| key == "name") + .map(|(_, value)| value.into_owned().into()); - let url_param = if let Some(query) = link_parsed.query() { - let prefix = "url="; - if let Some(pos) = query.find(prefix) { - let raw_url = &query[pos + prefix.len()..]; - Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) - } else { - None - } + let url_param = if let Some(query) = link_parsed.query() { + let prefix = "url="; + if let Some(pos) = query.find(prefix) { + let raw_url = &query[pos + prefix.len()..]; + Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) } else { None - }; - (url_param, name_owned) + } } else { - (None, None) + None }; + (url_param, name_owned) + } else { + (None, None) + }; let url = if let Some(ref url) = url_param { url } else { - logging!( - error, - Type::Config, - "missing url parameter in deep link: {}", - param_str - ); + logging!(error, Type::Config, "missing url parameter in deep link: {}", param_str); return Ok(()); }; let mut item = match PrfItem::from_url(url, name.as_ref(), None, None).await { Ok(item) => item, Err(e) => { - logging!( - error, - Type::Config, - "failed to parse profile from url: {:?}", - e - ); + logging!(error, Type::Config, "failed to parse profile from url: {:?}", e); handle::Handle::notice_message("import_sub_url::error", e.to_string()); return Ok(()); } @@ -94,12 +83,7 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> { handle::Handle::notify_profile_changed(uid); } Err(e) => { - logging!( - error, - Type::Config, - "failed to import subscription url: {:?}", - e - ); + logging!(error, Type::Config, "failed to import subscription url: {:?}", e); Config::profiles().await.discard(); handle::Handle::notice_message("import_sub_url::error", e.to_string()); return Ok(()); diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/window.rs b/clash-verge-rev/src-tauri/src/utils/resolve/window.rs index df0df0412c..24b78ed355 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/window.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/window.rs @@ -52,11 +52,7 @@ pub async fn build_new_window() -> Result { LIGHT_BACKGROUND_COLOR }; - let initial_script = build_window_initial_script( - initial_theme_mode, - DARK_BACKGROUND_HEX, - LIGHT_BACKGROUND_HEX, - ); + let initial_script = build_window_initial_script(initial_theme_mode, DARK_BACKGROUND_HEX, LIGHT_BACKGROUND_HEX); let mut builder = tauri::WebviewWindowBuilder::new( app_handle, @@ -81,10 +77,7 @@ pub async fn build_new_window() -> Result { match builder.build() { Ok(window) => { - logging_error!( - Type::Window, - window.set_background_color(Some(background_color)) - ); + logging_error!(Type::Window, window.set_background_color(Some(background_color))); logging_error!(Type::Window, window.eval(INITIAL_LOADING_OVERLAY)); Ok(window) } diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs b/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs index ff7062cd16..c8bd8ba001 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs @@ -1,8 +1,4 @@ -pub fn build_window_initial_script( - initial_theme_mode: &str, - dark_background: &str, - light_background: &str, -) -> String { +pub fn build_window_initial_script(initial_theme_mode: &str, dark_background: &str, light_background: &str) -> String { let theme_mode = match initial_theme_mode { "dark" => "dark", "light" => "light", diff --git a/clash-verge-rev/src-tauri/src/utils/server.rs b/clash-verge-rev/src-tauri/src/utils/server.rs index a8b96ff1a7..0dff946b4b 100644 --- a/clash-verge-rev/src-tauri/src/utils/server.rs +++ b/clash-verge-rev/src-tauri/src/utils/server.rs @@ -28,9 +28,7 @@ static SHUTDOWN_SENDER: OnceCell>>> = OnceCell: pub async fn check_singleton() -> Result<()> { let port = IVerge::get_singleton_port(); if !local_port_available(port) { - let client = ClientBuilder::new() - .timeout(Duration::from_millis(500)) - .build()?; + let client = ClientBuilder::new().timeout(Duration::from_millis(500)).build()?; // 需要确保 Send #[allow(clippy::needless_collect)] let argvs: Vec = std::env::args().collect(); @@ -40,9 +38,7 @@ pub async fn check_singleton() -> Result<()> { let param = argvs[1].as_str(); if param.starts_with("clash:") { client - .get(format!( - "http://127.0.0.1:{port}/commands/scheme?param={param}" - )) + .get(format!("http://127.0.0.1:{port}/commands/scheme?param={param}")) .send() .await?; } @@ -53,11 +49,7 @@ pub async fn check_singleton() -> Result<()> { .send() .await?; } - logging!( - error, - Type::Window, - "failed to setup singleton listen server" - ); + logging!(error, Type::Window, "failed to setup singleton listen server"); bail!("app exists"); } Ok(()) diff --git a/clash-verge-rev/src-tauri/src/utils/window_manager.rs b/clash-verge-rev/src-tauri/src/utils/window_manager.rs index 57cc11402f..28162ebc83 100644 --- a/clash-verge-rev/src-tauri/src/utils/window_manager.rs +++ b/clash-verge-rev/src-tauri/src/utils/window_manager.rs @@ -55,11 +55,7 @@ fn get_window_operation_debounce() -> &'static Mutex { fn should_handle_window_operation() -> bool { if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { - logging!( - warn, - Type::Window, - "Warning: [防抖] 窗口操作已在进行中,跳过重复调用" - ); + logging!(warn, Type::Window, "Warning: [防抖] 窗口操作已在进行中,跳过重复调用"); return false; } @@ -328,12 +324,7 @@ impl WindowManager { /// 创建新窗口,防抖避免重复调用 pub fn create_window(is_show: bool) -> Pin + Send>> { Box::pin(async move { - logging!( - info, - Type::Window, - "开始创建/显示主窗口, is_show={}", - is_show - ); + logging!(info, Type::Window, "开始创建/显示主窗口, is_show={}", is_show); if !is_show { return false; @@ -383,8 +374,6 @@ impl WindowManager { let is_focused = Self::is_main_window_focused(); let is_minimized = Self::is_main_window_minimized(); - format!( - "窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}" - ) + format!("窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}") } } diff --git a/clash-verge-rev/src-tauri/tauri.conf.json b/clash-verge-rev/src-tauri/tauri.conf.json index 4a50c25943..503438a7c3 100755 --- a/clash-verge-rev/src-tauri/tauri.conf.json +++ b/clash-verge-rev/src-tauri/tauri.conf.json @@ -1,5 +1,5 @@ { - "version": "2.4.4", + "version": "2.4.4-rc.1", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "bundle": { "active": true, diff --git a/clash-verge-rev/src-tauri/webview2.arm64.json b/clash-verge-rev/src-tauri/webview2.arm64.json index 6613e5fbb6..2e88117617 100644 --- a/clash-verge-rev/src-tauri/webview2.arm64.json +++ b/clash-verge-rev/src-tauri/webview2.arm64.json @@ -9,7 +9,7 @@ "timestampUrl": "", "webviewInstallMode": { "type": "fixedRuntime", - "path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.arm64/" + "path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.arm64/" }, "nsis": { "displayLanguageSelector": true, diff --git a/clash-verge-rev/src-tauri/webview2.x64.json b/clash-verge-rev/src-tauri/webview2.x64.json index a5063df7db..d3dccd78c7 100644 --- a/clash-verge-rev/src-tauri/webview2.x64.json +++ b/clash-verge-rev/src-tauri/webview2.x64.json @@ -9,7 +9,7 @@ "timestampUrl": "", "webviewInstallMode": { "type": "fixedRuntime", - "path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x64/" + "path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.x64/" }, "nsis": { "displayLanguageSelector": true, diff --git a/clash-verge-rev/src-tauri/webview2.x86.json b/clash-verge-rev/src-tauri/webview2.x86.json index b372b4b42b..8704931fbd 100644 --- a/clash-verge-rev/src-tauri/webview2.x86.json +++ b/clash-verge-rev/src-tauri/webview2.x86.json @@ -9,7 +9,7 @@ "timestampUrl": "", "webviewInstallMode": { "type": "fixedRuntime", - "path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x86/" + "path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.x86/" }, "nsis": { "displayLanguageSelector": true, diff --git a/clash-verge-rev/src/App.tsx b/clash-verge-rev/src/App.tsx deleted file mode 100644 index c1fe73a7d9..0000000000 --- a/clash-verge-rev/src/App.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Layout from "./pages/_layout"; -import { AppDataProvider } from "./providers/app-data-provider"; - -function App() { - return ( - - - - ); -} - -export default App; diff --git a/clash-verge-rev/src/components/base/NoticeManager.tsx b/clash-verge-rev/src/components/base/NoticeManager.tsx index dc00eac881..8bd59c7e2d 100644 --- a/clash-verge-rev/src/components/base/NoticeManager.tsx +++ b/clash-verge-rev/src/components/base/NoticeManager.tsx @@ -7,7 +7,7 @@ import { subscribeNotices, hideNotice, getSnapshotNotices, -} from "@/services/noticeService"; +} from "@/services/notice-service"; import type { TranslationKey } from "@/types/generated/i18n-keys"; export const NoticeManager: React.FC = () => { diff --git a/clash-verge-rev/src/components/center.tsx b/clash-verge-rev/src/components/center.tsx deleted file mode 100644 index 8204c32753..0000000000 --- a/clash-verge-rev/src/components/center.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Box, BoxProps } from "@mui/material"; -import React from "react"; - -interface CenterProps extends BoxProps { - children: React.ReactNode; -} - -export const Center: React.FC = ({ children, ...props }) => { - return ( - - {children} - - ); -}; diff --git a/clash-verge-rev/src/components/connection/connection-table.tsx b/clash-verge-rev/src/components/connection/connection-table.tsx index fd8eb46528..7309e2fac9 100644 --- a/clash-verge-rev/src/components/connection/connection-table.tsx +++ b/clash-verge-rev/src/components/connection/connection-table.tsx @@ -478,6 +478,8 @@ export const ConnectionTable = (props: Props) => { flex: 1, minHeight: 0, overflow: "auto", + WebkitOverflowScrolling: "touch", + overscrollBehavior: "contain", borderRadius: 1, border: "none", "&::-webkit-scrollbar": { diff --git a/clash-verge-rev/src/components/home/clash-info-card.tsx b/clash-verge-rev/src/components/home/clash-info-card.tsx index bb89c7d31e..7a4bff47f4 100644 --- a/clash-verge-rev/src/components/home/clash-info-card.tsx +++ b/clash-verge-rev/src/components/home/clash-info-card.tsx @@ -3,14 +3,14 @@ import { Divider, Stack, Typography } from "@mui/material"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { useClash } from "@/hooks/use-clash"; import { useAppUptime, useClashConfig, useRulesData, useSystemProxyAddress, useSystemProxyData, -} from "@/hooks/app-data"; -import { useClash } from "@/hooks/use-clash"; +} from "@/hooks/use-clash-data"; import { EnhancedCard } from "./enhanced-card"; diff --git a/clash-verge-rev/src/components/home/clash-mode-card.tsx b/clash-verge-rev/src/components/home/clash-mode-card.tsx index 2624ce2dd2..e6fb208417 100644 --- a/clash-verge-rev/src/components/home/clash-mode-card.tsx +++ b/clash-verge-rev/src/components/home/clash-mode-card.tsx @@ -9,7 +9,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { closeAllConnections } from "tauri-plugin-mihomo-api"; -import { useClashConfig } from "@/hooks/app-data"; +import { useClashConfig } from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; import { patchClashMode } from "@/services/cmds"; import type { TranslationKey } from "@/types/generated/i18n-keys"; diff --git a/clash-verge-rev/src/components/home/current-proxy-card.tsx b/clash-verge-rev/src/components/home/current-proxy-card.tsx index 2aabba29b8..6032152e90 100644 --- a/clash-verge-rev/src/components/home/current-proxy-card.tsx +++ b/clash-verge-rev/src/components/home/current-proxy-card.tsx @@ -34,7 +34,11 @@ import { useNavigate } from "react-router"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; import { EnhancedCard } from "@/components/home/enhanced-card"; -import { useClashConfig, useProxiesData, useRulesData } from "@/hooks/app-data"; +import { + useClashConfig, + useProxiesData, + useRulesData, +} from "@/hooks/use-clash-data"; import { useProfiles } from "@/hooks/use-profiles"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; diff --git a/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx b/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx index 9390b84571..e8aa255e18 100644 --- a/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -12,10 +12,7 @@ import { } from "react"; import { useTranslation } from "react-i18next"; -import { - useTrafficGraphDataEnhanced, - type ITrafficDataPoint, -} from "@/hooks/use-traffic-monitor"; +import { useTrafficGraphDataEnhanced } from "@/hooks/use-traffic-monitor"; import { debugLog } from "@/utils/debug"; import parseTraffic from "@/utils/parse-traffic"; diff --git a/clash-verge-rev/src/components/home/home-profile-card.tsx b/clash-verge-rev/src/components/home/home-profile-card.tsx index 8e40287f41..e92794ba48 100644 --- a/clash-verge-rev/src/components/home/home-profile-card.tsx +++ b/clash-verge-rev/src/components/home/home-profile-card.tsx @@ -24,9 +24,9 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; -import { useRefreshAll } from "@/hooks/app-data"; +import { useRefreshAll } from "@/hooks/use-clash-data"; import { openWebUrl, updateProfile } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import parseTraffic from "@/utils/parse-traffic"; import { EnhancedCard } from "./enhanced-card"; diff --git a/clash-verge-rev/src/components/home/proxy-tun-card.tsx b/clash-verge-rev/src/components/home/proxy-tun-card.tsx index e6d627613e..b17ee87803 100644 --- a/clash-verge-rev/src/components/home/proxy-tun-card.tsx +++ b/clash-verge-rev/src/components/home/proxy-tun-card.tsx @@ -17,11 +17,11 @@ import { import { useState, useMemo, memo, FC } from "react"; import { useTranslation } from "react-i18next"; -import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; +import ProxyControlSwitches from "@/components/shared/proxy-control-switches"; import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab"; diff --git a/clash-verge-rev/src/components/home/system-info-card.tsx b/clash-verge-rev/src/components/home/system-info-card.tsx index 8697291f56..4ec5f3b14e 100644 --- a/clash-verge-rev/src/components/home/system-info-card.tsx +++ b/clash-verge-rev/src/components/home/system-info-card.tsx @@ -20,11 +20,11 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; import useSWR from "swr"; +import { useServiceInstaller } from "@/hooks/use-service-installer"; import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; -import { useServiceInstaller } from "@/hooks/useServiceInstaller"; import { getSystemInfo } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { checkUpdateSafe as checkUpdate } from "@/services/update"; import { version as appVersion } from "@root/package.json"; diff --git a/clash-verge-rev/src/components/layout/layout-item.tsx b/clash-verge-rev/src/components/layout/layout-item.tsx index fdc2e1539c..6c6d23828d 100644 --- a/clash-verge-rev/src/components/layout/layout-item.tsx +++ b/clash-verge-rev/src/components/layout/layout-item.tsx @@ -37,9 +37,14 @@ export const LayoutItem = (props: Props) => { const match = useMatch({ path: resolved.pathname, end: true }); const navigate = useNavigate(); - const { setNodeRef, attributes, listeners, style, isDragging } = + const { setNodeRef, attributes, listeners, style, isDragging, disabled } = sortable ?? {}; + const draggable = Boolean(sortable) && !disabled; + const dragHandleProps = draggable + ? { ...(attributes ?? {}), ...(listeners ?? {}) } + : undefined; + return ( { > { paddingLeft: 1, paddingRight: 1, marginRight: 1.25, - cursor: "pointer", + cursor: draggable ? "grab" : "pointer", + "&:active": draggable ? { cursor: "grabbing" } : {}, "& .MuiListItemText-primary": { color: "text.primary", fontWeight: "700", @@ -84,20 +91,14 @@ export const LayoutItem = (props: Props) => { sx={{ color: "text.primary", marginLeft: "6px", - cursor: sortable && !sortable.disabled ? "grab" : "inherit", + cursor: draggable ? "grab" : "inherit", }} - {...(attributes ?? {})} - {...(listeners ?? {})} > {icon[0]} )} {menu_icon === "colorful" && ( - + {icon[1]} )} diff --git a/clash-verge-rev/src/components/profile/editor-viewer.tsx b/clash-verge-rev/src/components/profile/editor-viewer.tsx index 6ab2e0ae8f..a6d834f7f7 100644 --- a/clash-verge-rev/src/components/profile/editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/editor-viewer.tsx @@ -24,7 +24,7 @@ import { useTranslation } from "react-i18next"; import pac from "types-pac/pac.d.ts?raw"; import { BaseLoadingOverlay } from "@/components/base"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useThemeMode } from "@/services/states"; import debounce from "@/utils/debounce"; import getSystem from "@/utils/get-system"; diff --git a/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx b/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx index 0ccb5531fe..6e0d6ced94 100644 --- a/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/groups-editor-viewer.tsx @@ -55,7 +55,7 @@ import { readProfileFile, saveProfileFile, } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useThemeMode } from "@/services/states"; import type { TranslationKey } from "@/types/generated/i18n-keys"; import getSystem from "@/utils/get-system"; diff --git a/clash-verge-rev/src/components/profile/profile-item.tsx b/clash-verge-rev/src/components/profile/profile-item.tsx index 800c10238f..7d20e613b3 100644 --- a/clash-verge-rev/src/components/profile/profile-item.tsx +++ b/clash-verge-rev/src/components/profile/profile-item.tsx @@ -34,7 +34,7 @@ import { saveProfileFile, getNextUpdateTime, } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useLoadingCache, useSetLoadingCache } from "@/services/states"; import type { TranslationKey } from "@/types/generated/i18n-keys"; import { debugLog } from "@/utils/debug"; diff --git a/clash-verge-rev/src/components/profile/profile-more.tsx b/clash-verge-rev/src/components/profile/profile-more.tsx index 10d9017ab8..c607fa47df 100644 --- a/clash-verge-rev/src/components/profile/profile-more.tsx +++ b/clash-verge-rev/src/components/profile/profile-more.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { LogViewer } from "./log-viewer"; import { ProfileBox } from "./profile-box"; diff --git a/clash-verge-rev/src/components/profile/profile-viewer.tsx b/clash-verge-rev/src/components/profile/profile-viewer.tsx index 34b225e9cf..d441f26781 100644 --- a/clash-verge-rev/src/components/profile/profile-viewer.tsx +++ b/clash-verge-rev/src/components/profile/profile-viewer.tsx @@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; import { useProfiles } from "@/hooks/use-profiles"; import { createProfile, patchProfile } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { version } from "@root/package.json"; import { FileInput } from "./file-input"; diff --git a/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx b/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx index e8d4fe3b4e..a44529e92c 100644 --- a/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/proxies-editor-viewer.tsx @@ -42,7 +42,7 @@ import { Virtuoso } from "react-virtuoso"; import { ProxyItem } from "@/components/profile/proxy-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; import parseUri from "@/utils/uri-parser"; diff --git a/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx b/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx index 0eeab96291..861e5a9cf0 100644 --- a/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/rules-editor-viewer.tsx @@ -45,7 +45,7 @@ import { Virtuoso } from "react-virtuoso"; import { Switch } from "@/components/base"; import { RuleItem } from "@/components/profile/rule-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useThemeMode } from "@/services/states"; import type { TranslationKey } from "@/types/generated/i18n-keys"; import getSystem from "@/utils/get-system"; diff --git a/clash-verge-rev/src/components/proxy/provider-button.tsx b/clash-verge-rev/src/components/proxy/provider-button.tsx index 68217775a2..9009016122 100644 --- a/clash-verge-rev/src/components/proxy/provider-button.tsx +++ b/clash-verge-rev/src/components/proxy/provider-button.tsx @@ -22,8 +22,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateProxyProvider } from "tauri-plugin-mihomo-api"; -import { useProxiesData, useProxyProvidersData } from "@/hooks/app-data"; -import { showNotice } from "@/services/noticeService"; +import { useProxiesData, useProxyProvidersData } from "@/hooks/use-clash-data"; +import { showNotice } from "@/services/notice-service"; import parseTraffic from "@/utils/parse-traffic"; // 样式化组件 - 类型框 diff --git a/clash-verge-rev/src/components/proxy/proxy-chain.tsx b/clash-verge-rev/src/components/proxy/proxy-chain.tsx index b10676ce15..b93cb579ba 100644 --- a/clash-verge-rev/src/components/proxy/proxy-chain.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-chain.tsx @@ -39,7 +39,7 @@ import { selectNodeForGroup, } from "tauri-plugin-mihomo-api"; -import { useProxiesData } from "@/hooks/app-data"; +import { useProxiesData } from "@/hooks/use-clash-data"; import { calcuProxies, updateProxyChainConfigInRuntime } from "@/services/cmds"; import { debugLog } from "@/utils/debug"; @@ -285,23 +285,37 @@ export const ProxyChain = ({ const handleConnect = useCallback(async () => { if (isConnected) { - // 如果已连接,则断开连接 setIsConnecting(true); try { - // 清空链式代理配置 await updateProxyChainConfigInRuntime(null); - // 切换到 DIRECT 模式断开代理连接 - // await updateProxyAndSync("GLOBAL", "DIRECT"); + const targetGroup = + mode === "global" + ? "GLOBAL" + : selectedGroup || localStorage.getItem("proxy-chain-group"); + + if (targetGroup) { + try { + await selectNodeForGroup(targetGroup, "DIRECT"); + } catch { + if (proxyChain.length >= 1) { + try { + await selectNodeForGroup(targetGroup, proxyChain[0].name); + } catch { + // ignore + } + } + } + } + + localStorage.removeItem("proxy-chain-group"); + localStorage.removeItem("proxy-chain-exit-node"); + localStorage.removeItem("proxy-chain-items"); - // 关闭所有连接 await closeAllConnections(); + await mutateProxies(); - // 刷新代理信息以更新连接状态 - mutateProxies(); - - // 清空链式代理配置UI - // onUpdateChain([]); + onUpdateChain([]); } catch (error) { console.error("Failed to disconnect from proxy chain:", error); alert(t("proxies.page.chain.disconnectFailed") || "断开链式代理失败"); @@ -348,7 +362,15 @@ export const ProxyChain = ({ } finally { setIsConnecting(false); } - }, [proxyChain, isConnected, t, mutateProxies, mode, selectedGroup]); + }, [ + proxyChain, + isConnected, + t, + mutateProxies, + mode, + selectedGroup, + onUpdateChain, + ]); const proxyChainRef = useRef(proxyChain); const onUpdateChainRef = useRef(onUpdateChain); @@ -376,10 +398,11 @@ export const ProxyChain = ({ type: proxy.type, delay: undefined, })) || []; - onUpdateChain(chainItems); + if (chainItems.length > 0) { + onUpdateChain(chainItems); + } } catch (parseError) { console.error("Failed to parse YAML:", parseError); - onUpdateChain([]); } }) .catch((importError) => { @@ -399,19 +422,16 @@ export const ProxyChain = ({ type: proxy.type, delay: undefined, })) || []; - onUpdateChain(chainItems); + if (chainItems.length > 0) { + onUpdateChain(chainItems); + } } catch (jsonError) { console.error("Failed to parse as JSON either:", jsonError); - onUpdateChain([]); } }); } catch (error) { console.error("Failed to process chain config data:", error); - onUpdateChain([]); } - } else if (chainConfigData === "") { - // Empty string means no proxies available, show empty state - onUpdateChain([]); } }, [chainConfigData, onUpdateChain]); @@ -481,6 +501,9 @@ export const ProxyChain = ({ size="small" onClick={() => { updateProxyChainConfigInRuntime(null); + localStorage.removeItem("proxy-chain-group"); + localStorage.removeItem("proxy-chain-exit-node"); + localStorage.removeItem("proxy-chain-items"); onUpdateChain([]); }} sx={{ diff --git a/clash-verge-rev/src/components/proxy/proxy-groups.tsx b/clash-verge-rev/src/components/proxy/proxy-groups.tsx index ab4814262d..88d7012d81 100644 --- a/clash-verge-rev/src/components/proxy/proxy-groups.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-groups.tsx @@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; -import { useProxiesData } from "@/hooks/app-data"; +import { useProxiesData } from "@/hooks/use-clash-data"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; import { updateProxyChainConfigInRuntime } from "@/services/cmds"; @@ -27,8 +27,8 @@ import { ScrollTopButton } from "../layout/scroll-top-button"; import { ProxyChain } from "./proxy-chain"; import { - ProxyGroupNavigator, DEFAULT_HOVER_DELAY, + ProxyGroupNavigator, } from "./proxy-group-navigator"; import { ProxyRender } from "./proxy-render"; import { useRenderList } from "./use-render-list"; @@ -51,8 +51,26 @@ const VirtuosoFooter = () =>
; export const ProxyGroups = (props: Props) => { const { t } = useTranslation(); const { mode, isChainMode = false, chainConfigData } = props; - const [proxyChain, setProxyChain] = useState([]); + const [proxyChain, setProxyChain] = useState(() => { + try { + const saved = localStorage.getItem("proxy-chain-items"); + if (saved) { + return JSON.parse(saved); + } + } catch { + // ignore + } + return []; + }); const [selectedGroup, setSelectedGroup] = useState(null); + + useEffect(() => { + if (proxyChain.length > 0) { + localStorage.setItem("proxy-chain-items", JSON.stringify(proxyChain)); + } else { + localStorage.removeItem("proxy-chain-items"); + } + }, [proxyChain]); const [ruleMenuAnchor, setRuleMenuAnchor] = useState( null, ); @@ -64,7 +82,13 @@ export const ProxyGroups = (props: Props) => { const { verge } = useVerge(); const { proxies: proxiesData } = useProxiesData(); const groups = proxiesData?.groups; - const availableGroups = useMemo(() => groups ?? [], [groups]); + const availableGroups = useMemo(() => { + if (!groups) return []; + // 在链式代理模式下,仅显示支持选择节点的 Selector 代理组 + return isChainMode + ? groups.filter((g: any) => g.type === "Selector") + : groups; + }, [groups, isChainMode]); const defaultRuleGroup = useMemo(() => { if (isChainMode && mode === "rule" && availableGroups.length > 0) { @@ -225,10 +249,11 @@ export const ProxyGroups = (props: Props) => { setSelectedGroup(groupName); handleGroupMenuClose(); - // 在链式代理模式的规则模式下,切换代理组时清空链式代理配置 if (isChainMode && mode === "rule") { updateProxyChainConfigInRuntime(null); - // 同时清空右侧链式代理配置 + localStorage.removeItem("proxy-chain-group"); + localStorage.removeItem("proxy-chain-exit-node"); + localStorage.removeItem("proxy-chain-items"); setProxyChain([]); } }; diff --git a/clash-verge-rev/src/components/proxy/use-head-state.ts b/clash-verge-rev/src/components/proxy/use-head-state.ts index 593290f7b5..842a379d24 100644 --- a/clash-verge-rev/src/components/proxy/use-head-state.ts +++ b/clash-verge-rev/src/components/proxy/use-head-state.ts @@ -55,11 +55,6 @@ export function useHeadStateNew() { const [state, dispatch] = useReducer(headStateReducer, {}); useEffect(() => { - if (!current) { - dispatch({ type: "reset" }); - return; - } - try { const data = JSON.parse( localStorage.getItem(HEAD_STATE_KEY)!, @@ -78,8 +73,6 @@ export function useHeadStateNew() { }, [current]); useEffect(() => { - if (!current) return; - const timer = setTimeout(() => { try { const item = localStorage.getItem(HEAD_STATE_KEY); @@ -99,10 +92,9 @@ export function useHeadStateNew() { const setHeadState = useCallback( (groupName: string, obj: Partial) => { - if (!current) return; dispatch({ type: "update", groupName, patch: obj }); }, - [current], + [], ); return [state, setHeadState] as const; diff --git a/clash-verge-rev/src/components/proxy/use-render-list.ts b/clash-verge-rev/src/components/proxy/use-render-list.ts index 9e25c58ec2..dd7e96d6e2 100644 --- a/clash-verge-rev/src/components/proxy/use-render-list.ts +++ b/clash-verge-rev/src/components/proxy/use-render-list.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from "react"; import useSWR from "swr"; -import { useProxiesData } from "@/hooks/app-data"; +import { useProxiesData } from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; import { getRuntimeConfig } from "@/services/cmds"; import delayManager from "@/services/delay"; diff --git a/clash-verge-rev/src/components/rule/provider-button.tsx b/clash-verge-rev/src/components/rule/provider-button.tsx index 875be01b30..2db852482e 100644 --- a/clash-verge-rev/src/components/rule/provider-button.tsx +++ b/clash-verge-rev/src/components/rule/provider-button.tsx @@ -21,8 +21,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateRuleProvider } from "tauri-plugin-mihomo-api"; -import type { useRuleProvidersData, useRulesData } from "@/hooks/app-data"; -import { showNotice } from "@/services/noticeService"; +import type { + useRuleProvidersData, + useRulesData, +} from "@/hooks/use-clash-data"; +import { showNotice } from "@/services/notice-service"; // 辅助组件 - 类型框 const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ diff --git a/clash-verge-rev/src/components/setting/mods/auto-backup-settings.tsx b/clash-verge-rev/src/components/setting/mods/auto-backup-settings.tsx index c9736ec7a2..d8c2790b6e 100644 --- a/clash-verge-rev/src/components/setting/mods/auto-backup-settings.tsx +++ b/clash-verge-rev/src/components/setting/mods/auto-backup-settings.tsx @@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next"; import { Switch } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; const MIN_INTERVAL_HOURS = 1; const MAX_INTERVAL_HOURS = 168; diff --git a/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx index 5b5e98231d..1349fc9c37 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx @@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"; import { useVerge } from "@/hooks/use-verge"; import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { isValidUrl } from "@/utils/helper"; interface BackupConfigViewerProps { diff --git a/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx index 9b24ad25e6..d1be64b352 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx @@ -35,7 +35,7 @@ import { restoreLocalBackup, restoreWebDavBackup, } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; dayjs.extend(customParseFormat); dayjs.extend(relativeTime); diff --git a/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx index 185c45d6e6..17a4d8ecbe 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef } from "@/components/base"; import { createLocalBackup, createWebdavBackup } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { AutoBackupSettings } from "./auto-backup-settings"; import { BackupHistoryViewer } from "./backup-history-viewer"; diff --git a/clash-verge-rev/src/components/setting/mods/backup-webdav-dialog.tsx b/clash-verge-rev/src/components/setting/mods/backup-webdav-dialog.tsx index 8ad0e17a01..610350cc60 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-webdav-dialog.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-webdav-dialog.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, BaseLoadingOverlay } from "@/components/base"; import { listWebDavBackup } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { BackupConfigViewer } from "./backup-config-viewer"; diff --git a/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx b/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx index 614617788b..1cd4c31a9f 100644 --- a/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/clash-core-viewer.tsx @@ -21,7 +21,7 @@ import { closeAllConnections, upgradeCore } from "tauri-plugin-mihomo-api"; import { BaseDialog, DialogRef } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; import { changeClashCore, restartCore } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; const VALID_CORE = [ { diff --git a/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx b/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx index b0ed403fa5..a124e29a07 100644 --- a/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/clash-port-viewer.tsx @@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import getSystem from "@/utils/get-system"; const OS = getSystem(); diff --git a/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx b/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx index 7b10675910..7c5af0d6dd 100644 --- a/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/controller-viewer.tsx @@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; export function ControllerViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx b/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx index b1271226bd..650bae8f0e 100644 --- a/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx @@ -29,7 +29,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useThemeMode } from "@/services/states"; import { debugLog } from "@/utils/debug"; import getSystem from "@/utils/get-system"; diff --git a/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx b/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx index b4ded7c4fd..9fd4139a63 100644 --- a/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx +++ b/clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; // 定义开发环境的URL列表 // 这些URL在开发模式下会被自动包含在允许的来源中 diff --git a/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx b/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx index 8052bba1c7..870332388f 100644 --- a/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { HotkeyInput } from "./hotkey-input"; diff --git a/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx b/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx index ebc9588df1..e0988e8fcb 100644 --- a/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/layout-viewer.tsx @@ -23,7 +23,7 @@ import { DEFAULT_HOVER_DELAY } from "@/components/proxy/proxy-group-navigator"; import { useVerge } from "@/hooks/use-verge"; import { useWindowDecorations } from "@/hooks/use-window"; import { copyIconFile, getAppDir } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import getSystem from "@/utils/get-system"; import { GuardState } from "./guard-state"; diff --git a/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx b/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx index cabd10d329..87a4021c2d 100644 --- a/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/lite-mode-viewer.tsx @@ -15,7 +15,7 @@ import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; import { entry_lightweight_mode } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; export function LiteModeViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx b/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx index 62109e070b..b92051d0e0 100644 --- a/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/misc-viewer.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; export const MiscViewer = forwardRef((props, ref) => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx b/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx index fec78c0c34..71db18308a 100644 --- a/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/network-interface-viewer.tsx @@ -8,7 +8,7 @@ import useSWR from "swr"; import { BaseDialog, DialogRef } from "@/components/base"; import { getNetworkInterfacesInfo } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; export function NetworkInterfaceViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx b/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx index 4b7a4bd020..b647034a21 100644 --- a/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx @@ -30,7 +30,7 @@ import { useClashConfig, useSystemProxyAddress, useSystemProxyData, -} from "@/hooks/app-data"; +} from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; import { getAutotemProxy, @@ -39,7 +39,7 @@ import { getSystemProxy, patchVergeConfig, } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { debugLog } from "@/utils/debug"; import getSystem from "@/utils/get-system"; diff --git a/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx b/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx index 0f07f4ccc7..ee8be65cea 100644 --- a/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx @@ -23,7 +23,7 @@ import { BaseDialog, DialogRef } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { useVerge } from "@/hooks/use-verge"; import { defaultDarkTheme, defaultTheme } from "@/pages/_theme"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; export function ThemeViewer(props: { ref?: React.Ref }) { const { ref } = props; diff --git a/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx b/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx index 02bc984b17..1eb164d9d3 100644 --- a/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx @@ -16,7 +16,7 @@ import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; import { enhanceProfiles } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import getSystem from "@/utils/get-system"; import { StackModeSwitch } from "./stack-mode-switch"; @@ -33,7 +33,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { stack: "mixed", device: OS === "macos" ? "utun1024" : "Mihomo", autoRoute: true, - autoRedirect: OS === "linux", + autoRedirect: false, autoDetectInterface: true, dnsHijack: ["any:53"], strictRoute: false, @@ -44,7 +44,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { open: () => { setOpen(true); const nextAutoRoute = clash?.tun["auto-route"] ?? true; - const rawAutoRedirect = clash?.tun["auto-redirect"] ?? true; + const rawAutoRedirect = clash?.tun["auto-redirect"] ?? false; const computedAutoRedirect = OS === "linux" ? (nextAutoRoute ? rawAutoRedirect : false) : false; setValues({ @@ -118,7 +118,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { "auto-route": true, ...(OS === "linux" ? { - "auto-redirect": true, + "auto-redirect": false, } : {}), "auto-detect-interface": true, @@ -130,7 +130,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { stack: "gvisor", device: OS === "macos" ? "utun1024" : "Mihomo", autoRoute: true, - autoRedirect: OS === "linux" ? true : false, + autoRedirect: false, autoDetectInterface: true, dnsHijack: ["any:53"], strictRoute: false, diff --git a/clash-verge-rev/src/components/setting/mods/update-viewer.tsx b/clash-verge-rev/src/components/setting/mods/update-viewer.tsx index 739a07c750..95e22c4fb8 100644 --- a/clash-verge-rev/src/components/setting/mods/update-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/update-viewer.tsx @@ -11,7 +11,7 @@ import useSWR from "swr"; import { BaseDialog, DialogRef } from "@/components/base"; import { portableFlag } from "@/pages/_layout"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useSetUpdateState, useUpdateState } from "@/services/states"; import { checkUpdateSafe as checkUpdate } from "@/services/update"; diff --git a/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx b/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx index 5229f384f6..278864ec77 100644 --- a/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/web-ui-viewer.tsx @@ -8,7 +8,7 @@ import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { openWebUrl } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { WebUIItem } from "./web-ui-item"; diff --git a/clash-verge-rev/src/components/setting/setting-clash.tsx b/clash-verge-rev/src/components/setting/setting-clash.tsx index 931a17c6cc..ff182f72b3 100644 --- a/clash-verge-rev/src/components/setting/setting-clash.tsx +++ b/clash-verge-rev/src/components/setting/setting-clash.tsx @@ -9,10 +9,10 @@ import { updateGeo } from "tauri-plugin-mihomo-api"; import { DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; +import { useClashLog } from "@/hooks/use-clash-log"; import { useVerge } from "@/hooks/use-verge"; import { invoke_uwp_tool } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; -import { useClashLog } from "@/services/states"; +import { showNotice } from "@/services/notice-service"; import getSystem from "@/utils/get-system"; import { ClashCoreViewer } from "./mods/clash-core-viewer"; diff --git a/clash-verge-rev/src/components/setting/setting-system.tsx b/clash-verge-rev/src/components/setting/setting-system.tsx index bcaf47f65b..ebc69b8ad3 100644 --- a/clash-verge-rev/src/components/setting/setting-system.tsx +++ b/clash-verge-rev/src/components/setting/setting-system.tsx @@ -6,10 +6,10 @@ import { mutate } from "swr"; import { DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; +import ProxyControlSwitches from "@/components/shared/proxy-control-switches"; import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { GuardState } from "./mods/guard-state"; import { SettingList, SettingItem } from "./mods/setting-comp"; diff --git a/clash-verge-rev/src/components/setting/setting-verge-advanced.tsx b/clash-verge-rev/src/components/setting/setting-verge-advanced.tsx index 9434055f19..fa21467723 100644 --- a/clash-verge-rev/src/components/setting/setting-verge-advanced.tsx +++ b/clash-verge-rev/src/components/setting/setting-verge-advanced.tsx @@ -13,7 +13,7 @@ import { openDevTools, openLogsDir, } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { checkUpdateSafe as checkUpdate } from "@/services/update"; import { version } from "@root/package.json"; diff --git a/clash-verge-rev/src/components/setting/setting-verge-basic.tsx b/clash-verge-rev/src/components/setting/setting-verge-basic.tsx index 2f36677983..b298548da0 100644 --- a/clash-verge-rev/src/components/setting/setting-verge-basic.tsx +++ b/clash-verge-rev/src/components/setting/setting-verge-basic.tsx @@ -10,7 +10,7 @@ import { useVerge } from "@/hooks/use-verge"; import { navItems } from "@/pages/_routers"; import { copyClashEnv } from "@/services/cmds"; import { supportedLanguages } from "@/services/i18n"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import getSystem from "@/utils/get-system"; import { BackupViewer } from "./mods/backup-viewer"; diff --git a/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx b/clash-verge-rev/src/components/shared/proxy-control-switches.tsx similarity index 97% rename from clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx rename to clash-verge-rev/src/components/shared/proxy-control-switches.tsx index 79466dfb6c..0c712e3d85 100644 --- a/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx +++ b/clash-verge-rev/src/components/shared/proxy-control-switches.tsx @@ -16,12 +16,12 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { GuardState } from "@/components/setting/mods/guard-state"; import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer"; import { TunViewer } from "@/components/setting/mods/tun-viewer"; +import { useServiceInstaller } from "@/hooks/use-service-installer"; +import { useServiceUninstaller } from "@/hooks/use-service-uninstaller"; import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; -import { useServiceInstaller } from "@/hooks/useServiceInstaller"; -import { useServiceUninstaller } from "@/hooks/useServiceUninstaller"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; interface ProxySwitchProps { label?: string; diff --git a/clash-verge-rev/src/components/test/test-item.tsx b/clash-verge-rev/src/components/test/test-item.tsx index 63c181ced9..9f6743f1e8 100644 --- a/clash-verge-rev/src/components/test/test-item.tsx +++ b/clash-verge-rev/src/components/test/test-item.tsx @@ -12,7 +12,7 @@ import { BaseLoading } from "@/components/base"; import { useListen } from "@/hooks/use-listen"; import { cmdTestDelay, downloadIconCache } from "@/services/cmds"; import delayManager from "@/services/delay"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { debugLog } from "@/utils/debug"; import { TestBox } from "./test-box"; diff --git a/clash-verge-rev/src/components/test/test-viewer.tsx b/clash-verge-rev/src/components/test/test-viewer.tsx index 0ca2ebc68c..b69188d849 100644 --- a/clash-verge-rev/src/components/test/test-viewer.tsx +++ b/clash-verge-rev/src/components/test/test-viewer.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; interface Props { onChange: (uid: string, patch?: Partial) => void; diff --git a/clash-verge-rev/src/hooks/app-data.ts b/clash-verge-rev/src/hooks/use-clash-data.ts similarity index 100% rename from clash-verge-rev/src/hooks/app-data.ts rename to clash-verge-rev/src/hooks/use-clash-data.ts diff --git a/clash-verge-rev/src/hooks/use-clash-log.ts b/clash-verge-rev/src/hooks/use-clash-log.ts new file mode 100644 index 0000000000..66579f7984 --- /dev/null +++ b/clash-verge-rev/src/hooks/use-clash-log.ts @@ -0,0 +1,14 @@ +import { useLocalStorage } from "foxact/use-local-storage"; + +const defaultClashLog: IClashLog = { + enable: true, + logLevel: "info", + logFilter: "all", + logOrder: "asc", +}; + +export const useClashLog = () => + useLocalStorage("clash-log", defaultClashLog, { + serializer: JSON.stringify, + deserializer: JSON.parse, + }); diff --git a/clash-verge-rev/src/hooks/use-clash.ts b/clash-verge-rev/src/hooks/use-clash.ts index 85b3faedf9..8ee79119ad 100644 --- a/clash-verge-rev/src/hooks/use-clash.ts +++ b/clash-verge-rev/src/hooks/use-clash.ts @@ -8,6 +8,49 @@ import { getRuntimeConfig, } from "@/services/cmds"; +const PORT_KEYS = [ + "port", + "socks-port", + "mixed-port", + "redir-port", + "tproxy-port", +] as const; + +type ClashInfoPatch = Partial< + Pick< + IConfigData, + | "port" + | "socks-port" + | "mixed-port" + | "redir-port" + | "tproxy-port" + | "external-controller" + | "secret" + > +>; + +const hasClashInfoPayload = (patch: ClashInfoPatch) => + PORT_KEYS.some((key) => patch[key] != null) || + patch["external-controller"] != null || + patch.secret != null; + +const validatePortRange = (port: number) => { + if (port < 1000) { + throw new Error("The port should not < 1000"); + } + if (port > 65536) { + throw new Error("The port should not > 65536"); + } +}; + +const validatePorts = (patch: ClashInfoPatch) => { + PORT_KEYS.forEach((key) => { + const port = patch[key]; + if (!port) return; + validatePortRange(port); + }); +}; + export const useClash = () => { const { data: clash, mutate: mutateClash } = useSWR( "getRuntimeConfig", @@ -43,80 +86,10 @@ export const useClashInfo = () => { getClashInfo, ); - const patchInfo = async ( - patch: Partial< - Pick< - IConfigData, - | "port" - | "socks-port" - | "mixed-port" - | "redir-port" - | "tproxy-port" - | "external-controller" - | "secret" - > - >, - ) => { - const hasInfo = - patch["redir-port"] != null || - patch["tproxy-port"] != null || - patch["mixed-port"] != null || - patch["socks-port"] != null || - patch["port"] != null || - patch["external-controller"] != null || - patch.secret != null; + const patchInfo = async (patch: ClashInfoPatch) => { + if (!hasClashInfoPayload(patch)) return; - if (!hasInfo) return; - - if (patch["redir-port"]) { - const port = patch["redir-port"]; - if (port < 1000) { - throw new Error("The port should not < 1000"); - } - if (port > 65536) { - throw new Error("The port should not > 65536"); - } - } - - if (patch["tproxy-port"]) { - const port = patch["tproxy-port"]; - if (port < 1000) { - throw new Error("The port should not < 1000"); - } - if (port > 65536) { - throw new Error("The port should not > 65536"); - } - } - - if (patch["mixed-port"]) { - const port = patch["mixed-port"]; - if (port < 1000) { - throw new Error("The port should not < 1000"); - } - if (port > 65536) { - throw new Error("The port should not > 65536"); - } - } - - if (patch["socks-port"]) { - const port = patch["socks-port"]; - if (port < 1000) { - throw new Error("The port should not < 1000"); - } - if (port > 65536) { - throw new Error("The port should not > 65536"); - } - } - - if (patch["port"]) { - const port = patch["port"]; - if (port < 1000) { - throw new Error("The port should not < 1000"); - } - if (port > 65536) { - throw new Error("The port should not > 65536"); - } - } + validatePorts(patch); await patchClashConfig(patch); mutateInfo(); diff --git a/clash-verge-rev/src/hooks/use-cleanup.ts b/clash-verge-rev/src/hooks/use-cleanup.ts deleted file mode 100644 index 3766cec8ce..0000000000 --- a/clash-verge-rev/src/hooks/use-cleanup.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useRef } from "react"; - -/** - * 资源清理 Hook - * 用于在组件卸载或窗口关闭时统一清理资源 - */ -export const useCleanup = () => { - const cleanupFnsRef = useRef void>>(new Set()); - - const registerCleanup = (fn: () => void) => { - cleanupFnsRef.current.add(fn); - return () => { - cleanupFnsRef.current.delete(fn); - }; - }; - - const cleanup = () => { - cleanupFnsRef.current.forEach((fn) => { - try { - fn(); - } catch (error) { - console.error("[资源清理] 清理失败:", error); - } - }); - cleanupFnsRef.current.clear(); - }; - - useEffect(() => { - return () => { - cleanup(); - }; - }, []); - - return { registerCleanup, cleanup }; -}; diff --git a/clash-verge-rev/src/hooks/use-connection-data.ts b/clash-verge-rev/src/hooks/use-connection-data.ts index a4a103a003..15b41d25fa 100644 --- a/clash-verge-rev/src/hooks/use-connection-data.ts +++ b/clash-verge-rev/src/hooks/use-connection-data.ts @@ -1,9 +1,10 @@ -import { useLocalStorage } from "foxact/use-local-storage"; -import { useEffect, useRef } from "react"; import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; + +const MAX_CLOSED_CONNS_NUM = 500; + export const initConnData: ConnectionMonitorData = { uploadTotal: 0, downloadTotal: 0, @@ -18,129 +19,91 @@ export interface ConnectionMonitorData { closedConnections: IConnectionsItem[]; } -const MAX_CLOSED_CONNS_NUM = 500; +const trimClosedConnections = ( + closedConnections: IConnectionsItem[], +): IConnectionsItem[] => + closedConnections.length > MAX_CLOSED_CONNS_NUM + ? closedConnections.slice(-MAX_CLOSED_CONNS_NUM) + : closedConnections; + +const mergeConnectionSnapshot = ( + payload: IConnections, + previous: ConnectionMonitorData = initConnData, +): ConnectionMonitorData => { + const nextConnections = payload.connections ?? []; + const previousActive = previous.activeConnections ?? []; + const nextById = new Map(nextConnections.map((conn) => [conn.id, conn])); + const newIds = new Set(nextConnections.map((conn) => conn.id)); + + // Keep surviving connections in their previous relative order to reduce row reshuffle, + // but constrain the array to the incoming snapshot length. + const carried = previousActive + .map((prev) => { + const next = nextById.get(prev.id); + if (!next) return null; + + nextById.delete(prev.id); + return { + ...next, + curUpload: next.upload - prev.upload, + curDownload: next.download - prev.download, + } as IConnectionsItem; + }) + .filter(Boolean) as IConnectionsItem[]; + + const newcomers = nextConnections + .filter((conn) => nextById.has(conn.id)) + .map((conn) => ({ + ...conn, + curUpload: 0, + curDownload: 0, + })); + + const activeConnections = [...carried, ...newcomers]; + + const closedConnections = trimClosedConnections([ + ...(previous.closedConnections ?? []), + ...previousActive.filter((conn) => !newIds.has(conn.id)), + ]); + + return { + uploadTotal: payload.uploadTotal ?? 0, + downloadTotal: payload.downloadTotal ?? 0, + activeConnections, + closedConnections, + }; +}; export const useConnectionData = () => { - const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now()); - const subscriptKey = `getClashConnection-${date}`; - - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription< - ConnectionMonitorData, - any, - string | null - >( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); - }; - - const connect = () => - MihomoWebSocket.connect_connections() - .then((ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as IConnections; - next(null, (old = initConnData) => { - const oldConn = old.activeConnections; - const maxLen = data.connections?.length; - const activeConns: IConnectionsItem[] = []; - const rest = (data.connections || []).filter((each) => { - const index = oldConn.findIndex((o) => o.id === each.id); - if (index >= 0 && index < maxLen) { - const old = oldConn[index]; - each.curUpload = each.upload - old.upload; - each.curDownload = each.download - old.download; - activeConns[index] = each; - return false; - } - return true; - }); - for (let i = 0; i < maxLen; ++i) { - if (!activeConns[i] && rest.length > 0) { - activeConns[i] = rest.shift()!; - activeConns[i].curUpload = 0; - activeConns[i].curDownload = 0; - } - } - const currentClosedConns = oldConn.filter((each) => { - const index = activeConns.findIndex( - (o) => o.id === each.id, - ); - return index < 0; - }); - let closedConns = - old.closedConnections.concat(currentClosedConns); - if (closedConns.length > 500) { - closedConns = closedConns.slice(-MAX_CLOSED_CONNS_NUM); - } - return { - uploadTotal: data.uploadTotal, - downloadTotal: data.downloadTotal, - activeConnections: activeConns, - closedConnections: closedConns, - }; - }); - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; - } - connect(); - } - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - ws.current?.close(); - ws.current = null; - }; - }, - { + const { response, refresh, subscriptionCacheKey } = + useMihomoWsSubscription({ + storageKey: "mihomo_connection_date", + buildSubscriptKey: (date) => `getClashConnection-${date}`, fallbackData: initConnData, - keepPreviousData: true, - }, - ); + connect: () => MihomoWebSocket.connect_connections(), + setupHandlers: ({ next, scheduleReconnect }) => ({ + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data); + void scheduleReconnect(); + return; + } - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); - - const refreshGetClashConnection = () => { - setDate(Date.now()); - }; + try { + const parsed = JSON.parse(data) as IConnections; + next(null, (old = initConnData) => + mergeConnectionSnapshot(parsed, old), + ); + } catch (error) { + next(error); + } + }, + }), + }); const clearClosedConnections = () => { - mutate(`$sub$${subscriptKey}`, { + if (!subscriptionCacheKey) return; + mutate(subscriptionCacheKey, { uploadTotal: response.data?.uploadTotal ?? 0, downloadTotal: response.data?.downloadTotal ?? 0, activeConnections: response.data?.activeConnections ?? [], @@ -148,5 +111,9 @@ export const useConnectionData = () => { }); }; - return { response, refreshGetClashConnection, clearClosedConnections }; + return { + response, + refreshGetClashConnection: refresh, + clearClosedConnections, + }; }; diff --git a/clash-verge-rev/src/hooks/use-connection-setting.ts b/clash-verge-rev/src/hooks/use-connection-setting.ts new file mode 100644 index 0000000000..4b88ccab23 --- /dev/null +++ b/clash-verge-rev/src/hooks/use-connection-setting.ts @@ -0,0 +1,13 @@ +import { useLocalStorage } from "foxact/use-local-storage"; + +const defaultConnectionSetting: IConnectionSetting = { layout: "table" }; + +export const useConnectionSetting = () => + useLocalStorage( + "connections-setting", + defaultConnectionSetting, + { + serializer: JSON.stringify, + deserializer: JSON.parse, + }, + ); diff --git a/clash-verge-rev/src/hooks/use-current-proxy.ts b/clash-verge-rev/src/hooks/use-current-proxy.ts deleted file mode 100644 index e1d1ab1bab..0000000000 --- a/clash-verge-rev/src/hooks/use-current-proxy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useMemo } from "react"; - -import { useClashConfig, useProxiesData } from "@/hooks/app-data"; - -// 获取当前代理节点信息的自定义Hook -export const useCurrentProxy = () => { - const { proxies, refreshProxy } = useProxiesData(); - const { clashConfig } = useClashConfig(); - - // 获取当前模式 - const currentMode = clashConfig?.mode?.toLowerCase() || "rule"; - - // 获取当前代理节点信息 - const currentProxyInfo = useMemo(() => { - if (!proxies) return { currentProxy: null, primaryGroupName: null }; - - const globalGroup = proxies.global as IProxyGroupItem | undefined; - const groups: IProxyGroupItem[] = Array.isArray(proxies.groups) - ? (proxies.groups as IProxyGroupItem[]) - : []; - const records = (proxies.records || {}) as Record; - - // 默认信息 - let primaryGroupName = "GLOBAL"; - let currentName = globalGroup?.now; - - // 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组) - if (currentMode === "rule" && groups.length > 0) { - // 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组) - const primaryKeywords = [ - "auto", - "select", - "proxy", - "节点选择", - "自动选择", - ]; - const primaryGroup = - groups.find((group) => - primaryKeywords.some((keyword) => - group.name.toLowerCase().includes(keyword.toLowerCase()), - ), - ) || groups.filter((g) => g.name !== "GLOBAL")[0]; - - if (primaryGroup) { - primaryGroupName = primaryGroup.name; - currentName = primaryGroup.now; - } - } - - // 如果找不到当前节点,返回null - if (!currentName) return { currentProxy: null, primaryGroupName }; - - // 获取完整的节点信息 - const currentProxy = records[currentName] || { - name: currentName, - type: "Unknown", - udp: false, - xudp: false, - tfo: false, - mptcp: false, - smux: false, - history: [], - }; - - return { currentProxy, primaryGroupName }; - }, [proxies, currentMode]); - - return { - currentProxy: currentProxyInfo.currentProxy, - primaryGroupName: currentProxyInfo.primaryGroupName, - mode: currentMode, - refreshProxy, - }; -}; diff --git a/clash-verge-rev/src/hooks/use-listen.ts b/clash-verge-rev/src/hooks/use-listen.ts index f2bf4e9060..309754b668 100644 --- a/clash-verge-rev/src/hooks/use-listen.ts +++ b/clash-verge-rev/src/hooks/use-listen.ts @@ -1,20 +1,20 @@ import { event } from "@tauri-apps/api"; import { listen, UnlistenFn, EventCallback } from "@tauri-apps/api/event"; -import { useRef } from "react"; +import { useCallback, useRef } from "react"; export const useListen = () => { const unlistenFns = useRef([]); - const addListener = async ( - eventName: string, - handler: EventCallback, - ) => { - const unlisten = await listen(eventName, handler); - unlistenFns.current.push(unlisten); - return unlisten; - }; + const addListener = useCallback( + async (eventName: string, handler: EventCallback) => { + const unlisten = await listen(eventName, handler); + unlistenFns.current.push(unlisten); + return unlisten; + }, + [], + ); - const removeAllListeners = () => { + const removeAllListeners = useCallback(() => { const errors: Error[] = []; unlistenFns.current.forEach((unlisten) => { @@ -33,13 +33,13 @@ export const useListen = () => { } unlistenFns.current.length = 0; - }; + }, []); - const setupCloseListener = async function () { + const setupCloseListener = useCallback(async () => { await event.once("tauri://close-requested", async () => { removeAllListeners(); }); - }; + }, [removeAllListeners]); return { addListener, diff --git a/clash-verge-rev/src/hooks/use-log-data.ts b/clash-verge-rev/src/hooks/use-log-data.ts index 67f4ac1901..7fc26a6858 100644 --- a/clash-verge-rev/src/hooks/use-log-data.ts +++ b/clash-verge-rev/src/hooks/use-log-data.ts @@ -1,142 +1,113 @@ import dayjs from "dayjs"; -import { useLocalStorage } from "foxact/use-local-storage"; import { useEffect, useRef } from "react"; import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; -import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; +import { MihomoWebSocket, type LogLevel } from "tauri-plugin-mihomo-api"; import { getClashLogs } from "@/services/cmds"; -import { useClashLog } from "@/services/states"; + +import { useClashLog } from "./use-clash-log"; +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; const MAX_LOG_NUM = 1000; +const FLUSH_DELAY_MS = 50; +type LogType = ILogItem["type"]; + +const DEFAULT_LOG_TYPES: LogType[] = ["debug", "info", "warning", "error"]; +const LOG_LEVEL_FILTERS: Record = { + debug: DEFAULT_LOG_TYPES, + info: ["info", "warning", "error"], + warning: ["warning", "error"], + error: ["error"], + silent: [], +}; + +const clampLogs = (logs: ILogItem[]): ILogItem[] => + logs.length > MAX_LOG_NUM ? logs.slice(-MAX_LOG_NUM) : logs; + +const filterLogsByLevel = ( + logs: ILogItem[], + allowedTypes: LogType[], +): ILogItem[] => { + if (allowedTypes.length === 0) return []; + if (allowedTypes.length === DEFAULT_LOG_TYPES.length) return logs; + return logs.filter((log) => allowedTypes.includes(log.type)); +}; + +const appendLogs = ( + current: ILogItem[] | undefined, + incoming: ILogItem[], +): ILogItem[] => clampLogs([...(current ?? []), ...incoming]); export const useLogData = () => { const [clashLog] = useClashLog(); const enableLog = clashLog.enable; const logLevel = clashLog.logLevel; + const allowedTypes = LOG_LEVEL_FILTERS[logLevel] ?? DEFAULT_LOG_TYPES; - const [date, setDate] = useLocalStorage("mihomo_logs_date", Date.now()); - const subscriptKey = enableLog ? `getClashLog-${date}` : null; + const { response, refresh, subscriptionCacheKey } = useMihomoWsSubscription< + ILogItem[] + >({ + storageKey: "mihomo_logs_date", + buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null), + fallbackData: [], + keepPreviousData: true, + connect: () => MihomoWebSocket.connect_logs(logLevel), + setupHandlers: ({ next, scheduleReconnect, isMounted }) => { + let flushTimer: ReturnType | null = null; + const buffer: ILogItem[] = []; - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); + const clearFlushTimer = () => { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } }; - const connect = () => - MihomoWebSocket.connect_logs(logLevel) - .then(async (ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - const logs = await getClashLogs(); - let filterLogs: ILogItem[] = []; - switch (logLevel) { - case "debug": - filterLogs = logs.filter((i) => - ["debug", "info", "warning", "error"].includes(i.type), - ); - break; - case "info": - filterLogs = logs.filter((i) => - ["info", "warning", "error"].includes(i.type), - ); - break; - case "warning": - filterLogs = logs.filter((i) => - ["warning", "error"].includes(i.type), - ); - break; - case "error": - filterLogs = logs.filter((i) => i.type === "error"); - break; - case "silent": - filterLogs = []; - break; - default: - filterLogs = logs; - break; - } - next(null, filterLogs); - - const buffer: ILogItem[] = []; - let flushTimer: ReturnType | null = null; - const flush = () => { - if (buffer.length > 0) { - next(null, (l) => { - let newList = [...(l ?? []), ...buffer.splice(0)]; - if (newList.length > MAX_LOG_NUM) { - newList = newList.slice( - -Math.min(MAX_LOG_NUM, newList.length), - ); - } - return newList; - }); - } - flushTimer = null; - }; - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as ILogItem; - data.time = dayjs().format("MM-DD HH:mm:ss"); - buffer.push(data); - - // flush data - if (!flushTimer) { - flushTimer = setTimeout(flush, 50); - } - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; + const flush = () => { + if (!buffer.length || !isMounted()) { + flushTimer = null; + return; } - connect(); - } + const pendingLogs = buffer.splice(0, buffer.length); + next(null, (current) => appendLogs(current, pendingLogs)); + flushTimer = null; + }; - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - ws.current?.close(); - ws.current = null; + return { + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data); + void scheduleReconnect(); + return; + } + + try { + const parsed = JSON.parse(data) as ILogItem; + if ( + allowedTypes.length > 0 && + !allowedTypes.includes(parsed.type) + ) { + return; + } + parsed.time = dayjs().format("MM-DD HH:mm:ss"); + buffer.push(parsed); + if (!flushTimer) { + flushTimer = setTimeout(flush, FLUSH_DELAY_MS); + } + } catch (error) { + next(error); + } + }, + async onConnected() { + const logs = await getClashLogs(); + if (isMounted()) { + next(null, clampLogs(filterLogsByLevel(logs, allowedTypes))); + } + }, + cleanup: clearFlushTimer, }; }, - { - fallbackData: [], - keepPreviousData: true, - }, - ); - - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); + }); const previousLogLevel = useRef(undefined); @@ -151,15 +122,16 @@ export const useLogData = () => { } previousLogLevel.current = logLevel; - ws.current?.close(); - setDate(Date.now()); - }, [logLevel, setDate]); + refresh(); + }, [logLevel, refresh]); const refreshGetClashLog = (clear = false) => { if (clear) { - mutate(`$sub$${subscriptKey}`, []); + if (subscriptionCacheKey) { + mutate(subscriptionCacheKey, []); + } } else { - setDate(Date.now()); + refresh(); } }; diff --git a/clash-verge-rev/src/hooks/use-memory-data.ts b/clash-verge-rev/src/hooks/use-memory-data.ts index 12b25d3409..2a7d78da35 100644 --- a/clash-verge-rev/src/hooks/use-memory-data.ts +++ b/clash-verge-rev/src/hooks/use-memory-data.ts @@ -1,89 +1,37 @@ -import { useLocalStorage } from "foxact/use-local-storage"; -import { useEffect, useRef } from "react"; -import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; + export interface IMemoryUsageItem { inuse: number; oslimit?: number; } +const FALLBACK_MEMORY_USAGE: IMemoryUsageItem = { inuse: 0 }; + export const useMemoryData = () => { - const [date, setDate] = useLocalStorage("mihomo_memory_date", Date.now()); - const subscriptKey = `getClashMemory-${date}`; - - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); - }; - - const connect = () => - MihomoWebSocket.connect_memory() - .then((ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data, { inuse: 0 }); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as IMemoryUsageItem; - next(null, data); - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; + const { response, refresh } = useMihomoWsSubscription({ + storageKey: "mihomo_memory_date", + buildSubscriptKey: (date) => `getClashMemory-${date}`, + fallbackData: FALLBACK_MEMORY_USAGE, + connect: () => MihomoWebSocket.connect_memory(), + setupHandlers: ({ next, scheduleReconnect }) => ({ + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data, FALLBACK_MEMORY_USAGE); + void scheduleReconnect(); + return; } - connect(); - } - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + try { + const parsed = JSON.parse(data) as IMemoryUsageItem; + next(null, parsed); + } catch (error) { + next(error, FALLBACK_MEMORY_USAGE); } - ws.current?.close(); - ws.current = null; - }; - }, - { - fallbackData: { inuse: 0 }, - keepPreviousData: true, - }, - ); + }, + }), + }); - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); - - const refreshGetClashMemory = () => { - setDate(Date.now()); - }; - - return { response, refreshGetClashMemory }; + return { response, refreshGetClashMemory: refresh }; }; diff --git a/clash-verge-rev/src/hooks/use-mihomo-ws-subscription.ts b/clash-verge-rev/src/hooks/use-mihomo-ws-subscription.ts new file mode 100644 index 0000000000..f2cdad1047 --- /dev/null +++ b/clash-verge-rev/src/hooks/use-mihomo-ws-subscription.ts @@ -0,0 +1,156 @@ +import { useLocalStorage } from "foxact/use-local-storage"; +import { useCallback, useEffect, useRef } from "react"; +import { mutate, type MutatorCallback } from "swr"; +import useSWRSubscription from "swr/subscription"; +import { type Message, type MihomoWebSocket } from "tauri-plugin-mihomo-api"; + +export const RECONNECT_DELAY_MS = 500; + +type NextFn = (error?: any, data?: T | MutatorCallback) => void; + +interface HandlerContext { + next: NextFn; + scheduleReconnect: () => Promise; + isMounted: () => boolean; +} + +interface HandlerResult { + handleMessage: (data: string) => void; + onConnected?: (ws: MihomoWebSocket) => Promise | void; + cleanup?: () => void; +} + +interface UseMihomoWsSubscriptionOptions { + storageKey: string; + buildSubscriptKey: (date: number) => string | null; + fallbackData: T; + connect: () => Promise; + keepPreviousData?: boolean; + setupHandlers: (ctx: HandlerContext) => HandlerResult; +} + +export const useMihomoWsSubscription = ( + options: UseMihomoWsSubscriptionOptions, +) => { + const { + storageKey, + buildSubscriptKey, + fallbackData, + connect, + keepPreviousData = true, + setupHandlers, + } = options; + + const [date, setDate] = useLocalStorage(storageKey, Date.now()); + const subscriptKey = buildSubscriptKey(date); + const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null; + + const wsRef = useRef(null); + const wsFirstConnection = useRef(true); + const timeoutRef = useRef | null>(null); + + const response = useSWRSubscription( + subscriptKey, + (_key, { next }) => { + let isMounted = true; + + const clearReconnectTimer = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + + const closeSocket = async () => { + if (wsRef.current) { + await wsRef.current.close(); + wsRef.current = null; + } + }; + + const scheduleReconnect = async () => { + if (!isMounted) return; + clearReconnectTimer(); + await closeSocket(); + if (!isMounted) return; + timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS); + }; + + const { + handleMessage: handleTextMessage, + onConnected, + cleanup, + } = setupHandlers({ + next, + scheduleReconnect, + isMounted: () => isMounted, + }); + + const cleanupAll = () => { + clearReconnectTimer(); + cleanup?.(); + void closeSocket(); + }; + + const handleMessage = (msg: Message) => { + if (msg.type !== "Text") return; + handleTextMessage(msg.data); + }; + + async function connectWs() { + try { + const ws_ = await connect(); + if (!isMounted) { + await ws_.close(); + return; + } + + wsRef.current = ws_; + clearReconnectTimer(); + + if (onConnected) { + await onConnected(ws_); + if (!isMounted) { + await ws_.close(); + return; + } + } + + ws_.addListener(handleMessage); + } catch (ignoreError) { + if (!wsRef.current && isMounted) { + timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS); + } + } + } + + if (wsFirstConnection.current || !wsRef.current) { + wsFirstConnection.current = false; + cleanupAll(); + void connectWs(); + } + + return () => { + isMounted = false; + wsFirstConnection.current = true; + cleanupAll(); + }; + }, + { + fallbackData, + keepPreviousData, + }, + ); + + useEffect(() => { + if (subscriptionCacheKey) { + mutate(subscriptionCacheKey); + } + }, [subscriptionCacheKey]); + + const refresh = useCallback(() => { + setDate(Date.now()); + }, [setDate]); + + return { response, refresh, subscriptionCacheKey, wsRef }; +}; diff --git a/clash-verge-rev/src/hooks/useServiceInstaller.ts b/clash-verge-rev/src/hooks/use-service-installer.ts similarity index 95% rename from clash-verge-rev/src/hooks/useServiceInstaller.ts rename to clash-verge-rev/src/hooks/use-service-installer.ts index 51985980df..efb5fd3e76 100644 --- a/clash-verge-rev/src/hooks/useServiceInstaller.ts +++ b/clash-verge-rev/src/hooks/use-service-installer.ts @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { installService, restartCore } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useSystemState } from "./use-system-state"; diff --git a/clash-verge-rev/src/hooks/useServiceUninstaller.ts b/clash-verge-rev/src/hooks/use-service-uninstaller.ts similarity index 95% rename from clash-verge-rev/src/hooks/useServiceUninstaller.ts rename to clash-verge-rev/src/hooks/use-service-uninstaller.ts index a02c272a52..5685be6001 100644 --- a/clash-verge-rev/src/hooks/useServiceUninstaller.ts +++ b/clash-verge-rev/src/hooks/use-service-uninstaller.ts @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { restartCore, stopCore, uninstallService } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useSystemState } from "./use-system-state"; diff --git a/clash-verge-rev/src/hooks/use-system-proxy-state.ts b/clash-verge-rev/src/hooks/use-system-proxy-state.ts index ef25274b19..b6dc947520 100644 --- a/clash-verge-rev/src/hooks/use-system-proxy-state.ts +++ b/clash-verge-rev/src/hooks/use-system-proxy-state.ts @@ -2,7 +2,7 @@ import { useLockFn } from "ahooks"; import useSWR, { mutate } from "swr"; import { closeAllConnections } from "tauri-plugin-mihomo-api"; -import { useSystemProxyData } from "@/hooks/app-data"; +import { useSystemProxyData } from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; import { getAutotemProxy } from "@/services/cmds"; diff --git a/clash-verge-rev/src/hooks/use-system-state.ts b/clash-verge-rev/src/hooks/use-system-state.ts index 474a743df5..58f14d55aa 100644 --- a/clash-verge-rev/src/hooks/use-system-state.ts +++ b/clash-verge-rev/src/hooks/use-system-state.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import useSWR from "swr"; import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useVerge } from "./use-verge"; diff --git a/clash-verge-rev/src/hooks/use-traffic-data.ts b/clash-verge-rev/src/hooks/use-traffic-data.ts index eb8ca41110..4739927b13 100644 --- a/clash-verge-rev/src/hooks/use-traffic-data.ts +++ b/clash-verge-rev/src/hooks/use-traffic-data.ts @@ -1,95 +1,37 @@ -import { useLocalStorage } from "foxact/use-local-storage"; -import { useEffect, useRef } from "react"; -import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket, Traffic } from "tauri-plugin-mihomo-api"; -import { TrafficRef } from "@/components/layout/traffic-graph"; - +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; import { useTrafficMonitorEnhanced } from "./use-traffic-monitor"; -export const useTrafficData = () => { - const [date, setDate] = useLocalStorage("mihomo_traffic_date", Date.now()); - const subscriptKey = `getClashTraffic-${date}`; +const FALLBACK_TRAFFIC: Traffic = { up: 0, down: 0 }; - const trafficRef = useRef(null); +export const useTrafficData = () => { const { graphData: { appendData }, } = useTrafficMonitorEnhanced({ subscribe: false }); - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); - }; - - const connect = async () => { - MihomoWebSocket.connect_traffic() - .then(async (ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data, { up: 0, down: 0 }); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as Traffic; - trafficRef.current?.appendData(data); - appendData(data); - next(null, data); - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - }; - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; + const { response, refresh } = useMihomoWsSubscription({ + storageKey: "mihomo_traffic_date", + buildSubscriptKey: (date) => `getClashTraffic-${date}`, + fallbackData: FALLBACK_TRAFFIC, + connect: () => MihomoWebSocket.connect_traffic(), + setupHandlers: ({ next, scheduleReconnect }) => ({ + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data, FALLBACK_TRAFFIC); + void scheduleReconnect(); + return; } - connect(); - } - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + try { + const parsed = JSON.parse(data) as Traffic; + appendData(parsed); + next(null, parsed); + } catch (error) { + next(error, FALLBACK_TRAFFIC); } - ws.current?.close(); - ws.current = null; - }; - }, - { - fallbackData: { up: 0, down: 0 }, - keepPreviousData: true, - }, - ); + }, + }), + }); - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); - - const refreshGetClashTraffic = () => { - setDate(Date.now()); - }; - - return { response, refreshGetClashTraffic }; + return { response, refreshGetClashTraffic: refresh }; }; diff --git a/clash-verge-rev/src/hooks/use-traffic-monitor.ts b/clash-verge-rev/src/hooks/use-traffic-monitor.ts index 0f1291be61..852b53bb53 100644 --- a/clash-verge-rev/src/hooks/use-traffic-monitor.ts +++ b/clash-verge-rev/src/hooks/use-traffic-monitor.ts @@ -8,18 +8,9 @@ import { } from "react"; import { Traffic } from "tauri-plugin-mihomo-api"; -import type { - ISamplerStats, - ITrafficDataPoint, - ITrafficWorkerSnapshotMessage, - TrafficWorkerRequestMessage, - TrafficWorkerResponseMessage, -} from "@/types/traffic-monitor"; import { debugLog } from "@/utils/debug"; import { TrafficDataSampler, formatTrafficName } from "@/utils/traffic-sampler"; -export type { ITrafficDataPoint } from "@/types/traffic-monitor"; - // 引用计数管理器 class ReferenceCounter { private count = 0; diff --git a/clash-verge-rev/src/locales/ar/layout.json b/clash-verge-rev/src/locales/ar/layout.json index fba5415b13..03187e6dd8 100644 --- a/clash-verge-rev/src/locales/ar/layout.json +++ b/clash-verge-rev/src/locales/ar/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/de/layout.json b/clash-verge-rev/src/locales/de/layout.json index b3026730ca..c9971651ba 100644 --- a/clash-verge-rev/src/locales/de/layout.json +++ b/clash-verge-rev/src/locales/de/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/en/layout.json b/clash-verge-rev/src/locales/en/layout.json index 7fafd75261..5bf8c84024 100644 --- a/clash-verge-rev/src/locales/en/layout.json +++ b/clash-verge-rev/src/locales/en/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/en/rules.json b/clash-verge-rev/src/locales/en/rules.json index 3753149220..2f2dbc5beb 100644 --- a/clash-verge-rev/src/locales/en/rules.json +++ b/clash-verge-rev/src/locales/en/rules.json @@ -46,7 +46,7 @@ "DOMAIN-SUFFIX": "Match domain suffix (DOMAIN-SUFFIX)", "DOMAIN-KEYWORD": "Match domain keyword (DOMAIN-KEYWORD)", "DOMAIN-REGEX": "Match domain using regex (DOMAIN-REGEX)", - "GEOSITE": "Match domains in Geosite (GEOSITE)", + "GEOSITE": "Match domains in GeoSite (GEOSITE)", "GEOIP": "Match IP country code (GEOIP)", "SRC-GEOIP": "Match source IP country code (SRC-GEOIP)", "IP-ASN": "Match IP ASN (IP-ASN)", diff --git a/clash-verge-rev/src/locales/es/layout.json b/clash-verge-rev/src/locales/es/layout.json index 217d6d706f..f2e6849dfe 100644 --- a/clash-verge-rev/src/locales/es/layout.json +++ b/clash-verge-rev/src/locales/es/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/fa/layout.json b/clash-verge-rev/src/locales/fa/layout.json index 5a7239845e..26731f4bde 100644 --- a/clash-verge-rev/src/locales/fa/layout.json +++ b/clash-verge-rev/src/locales/fa/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/fa/settings.json b/clash-verge-rev/src/locales/fa/settings.json index ee9fd2467e..23153f62cf 100644 --- a/clash-verge-rev/src/locales/fa/settings.json +++ b/clash-verge-rev/src/locales/fa/settings.json @@ -15,7 +15,7 @@ "systemProxy": "پراکسی سیستم" }, "tooltips": { - "autoLaunchAdmin": "Administrator mode may not support auto launch", + "autoLaunchAdmin": "حالت ادمین ممکن است از راه‌اندازی خودکار پشتیبانی نکند", "silentStart": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید" }, "fields": { @@ -24,8 +24,8 @@ }, "notifications": { "tunMode": { - "autoDisabled": "TUN Mode automatically disabled due to service unavailable", - "autoDisableFailed": "Failed to disable TUN Mode automatically" + "autoDisabled": "حالت TUN به دلیل عدم دسترسی به سرویس، به طور خودکار غیرفعال می‌شود", + "autoDisableFailed": "غیرفعال کردن خودکار حالت TUN ناموفق بود" } } }, @@ -33,11 +33,11 @@ "tooltips": { "systemProxy": "به امکانات تنظیم پروکسی سیستم عامل دسترسی پیدا کنید. اگر فعال‌سازی ناموفق بود، پروکسی سیستم عامل را به‌صورت دستی تغییر دهید", "tunMode": "حالت Tun (NIC مجازی): تمام ترافیک سیستم را ضبط می کند، وقتی فعال باشد، نیازی به فعال کردن پروکسی سیستم نیست.", - "tunUnavailable": "TUN requires Service Mode or Admin Mode" + "tunUnavailable": "TUN به حالت سرویس یا حالت ادمین نیاز دارد" }, "actions": { "installService": "نصب سرویس", - "uninstallService": "Uninstall Service" + "uninstallService": "حذف سرویس" }, "fields": { "systemProxy": "پراکسی سیستم", @@ -47,22 +47,22 @@ "externalController": { "title": "کنترل‌کننده خارجی", "fields": { - "enable": "Enable External Controller", + "enable": "فعال کردن کنترل‌کننده خارجی", "address": "کنترل‌کننده خارجی", "secret": "رمز اصلی" }, "placeholders": { - "address": "Required", + "address": "مورد نیاز", "secret": "توصیه شده" }, "tooltips": { - "copy": "Copy to clipboard" + "copy": "کپی در کلیپ بورد" }, "messages": { - "addressRequired": "Controller address cannot be empty", - "secretRequired": "Secret cannot be empty", - "copyFailed": "Failed to copy", - "controllerCopied": "Controller address copied to clipboard", + "addressRequired": "آدرس کنترلر نمی‌تواند خالی باشد", + "secretRequired": "راز نمی‌تواند خالی باشد", + "copyFailed": "کپی نشد", + "controllerCopied": "آدرس کنترلر در کلیپ بورد کپی شد", "secretCopied": "Secret copied to clipboard" } }, @@ -73,10 +73,10 @@ "allowedOrigins": "Allowed Origins" }, "placeholders": { - "origin": "Please enter a valid url" + "origin": "لطفا یک url معتبر وارد کنید" }, "actions": { - "add": "Add" + "add": "افزودن" }, "messages": { "alwaysIncluded": "Always included origins: {{urls}}" @@ -95,7 +95,7 @@ "form": { "fields": { "allowLan": "اجازه LAN", - "dnsOverwrite": "DNS Overwrite", + "dnsOverwrite": "بازنویسی DNS", "ipv6": "IPv6", "unifiedDelay": "معادلDELAY", "logLevel": "سطح لاگ", @@ -157,11 +157,11 @@ "liteMode": "رابط کاربری گرافیکی را ببندید و فقط هسته را در حال اجرا نگه دارید" }, "actions": { - "copyVersion": "Copy Version" + "copyVersion": "کپی نسخه" }, "notifications": { "latestVersion": "در حال حاضر در آخرین نسخه", - "versionCopied": "Version copied to clipboard" + "versionCopied": "نسخه در کلیپ بورد کپی شد" }, "fields": { "backupSetting": "تنظیمات پشتیبان گیری", @@ -263,13 +263,13 @@ "autoEnter": "Auto Enter LightWeight Mode" }, "tooltips": { - "autoEnter": "Enable to automatically activate LightWeight Mode after the window is closed for a period of time" + "autoEnter": "فعال کردن حالت LightWeight به صورت خودکار پس از بسته شدن پنجره برای مدت زمانی خاص" }, "fields": { - "delay": "Auto Enter LightWeight Mode Delay" + "delay": "تأخیر ورود خودکار به حالت LightWeight" }, "messages": { - "autoEnterHint": "When closing the window, LightWeight Mode will be automatically activated after {{n}} minutes" + "autoEnterHint": "هنگام بستن پنجره، حالت LightWeight پس از {{n}} دقیقه به طور خودکار فعال می‌شود" } }, "backup": { @@ -325,18 +325,18 @@ } }, "manual": { - "title": "Manual backup", - "local": "Creates a snapshot on this device, stored under the app data directory.", - "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", - "configureWebdav": "Configure WebDAV" + "title": "پشتیبان گیری دستی", + "local": "یک اسنپ‌شات روی این دستگاه ایجاد می‌کند که در پوشه‌ی داده‌های برنامه ذخیره می‌شود.", + "webdav": "پس از تنظیم اعتبارنامه‌ها، یک اسنپ‌شات (عکس فوری) در سرور WebDAV خود آپلود کنید.", + "configureWebdav": "پیکربندی WebDAV" }, "history": { - "title": "Backup history", + "title": "تاریخچه پشتیبان گیری", "summary": "{{count}} backups • latest {{recent}}", - "empty": "No backups available" + "empty": "هیچ نسخه پشتیبان در دسترس نیست" }, "webdav": { - "title": "WebDAV settings" + "title": "پیکربندی WebDAV" }, "table": { "filename": "نام فایل", @@ -431,8 +431,8 @@ "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers", - "autoRedirect": "Automatically configures nftables/iptables TCP redirects" + "dnsHijack": "لطفا برای جدا کردن چندین سرور DNS از , استفاده کنید.", + "autoRedirect": "پیکربندی خودکار ریدایرکت‌های TCP در nftables/iptables" }, "messages": { "applied": "تنظیمات اعمال شد" @@ -440,11 +440,11 @@ }, "dns": { "dialog": { - "title": "DNS Overwrite", - "warning": "If you are not familiar with these settings, please do not modify them and keep DNS Overwrite enabled" + "title": "بازنویسی DNS", + "warning": "اگر با این تنظیمات آشنا نیستید، لطفاً آنها را تغییر ندهید و DNS Overwrite را فعال نگه دارید." }, "sections": { - "general": "DNS Settings", + "general": "تنظیمات DNS", "fallbackFilter": "Fallback Filter Settings", "hosts": "Hosts Settings" }, @@ -464,7 +464,7 @@ }, "respectRules": { "label": "Respect Rules", - "description": "DNS connections follow routing rules" + "description": "اتصالات DNS از قوانین مسیریابی پیروی می‌کنند" }, "useHosts": { "label": "Use Hosts", @@ -484,11 +484,11 @@ }, "nameserver": { "label": "Nameserver", - "description": "List of DNS servers, comma separated" + "description": "فهرست سرورهای DNS، جدا شده با کاما" }, "fallback": { "label": "Fallback", - "description": "List of fallback DNS servers, comma separated" + "description": "فهرست سرورهای DNS جایگزین، جدا شده با کاما" }, "proxy": { "label": "Proxy Server Nameserver", @@ -516,21 +516,21 @@ "description": "IP CIDRs not using fallback servers, comma separated" }, "fallbackDomain": { - "label": "Fallback Domain", - "description": "Domains using fallback servers, comma separated" + "label": "دامنه جایگزین", + "description": "دامنه‌هایی که از سرورهای جایگزین استفاده می‌کنند، با کاما از هم جدا شده‌اند" }, "hosts": { - "label": "Hosts", - "description": "Custom domain to IP or domain mapping" + "label": "میزبان‌ها", + "description": "تبدیل دامنه به IP یا نگاشت دامنه سفارشی" } }, "messages": { - "saved": "DNS settings saved", - "configError": "DNS configuration error:" + "saved": "تنظیمات DNS ذخیره شد", + "configError": "خطای پیکربندی DNS:" }, "errors": { - "invalid": "Invalid configuration", - "invalidYaml": "Invalid YAML format" + "invalid": "پیکربندی نامعتبر", + "invalidYaml": "قالب YAML نامعتبر است" } }, "webUI": { @@ -587,8 +587,8 @@ "uninstallSuccess": "سرویس با موفقیت حذف نصب شد" }, "updater": { - "withClashProxySuccess": "Update with Clash proxy successfully", - "withClashProxyFailed": "Update failed even with Clash proxy" + "withClashProxySuccess": "با موفقیت با پروکسی کلش به‌روزرسانی شد", + "withClashProxyFailed": "به‌روزرسانی حتی با پروکسی کلش هم انجام نشد" } } }, @@ -599,7 +599,7 @@ }, "clashService": { "installing": "در حال نصب سرویس...", - "uninstalling": "Uninstalling Service..." + "uninstalling": "در حال حذف سرویس..." } } } diff --git a/clash-verge-rev/src/locales/id/layout.json b/clash-verge-rev/src/locales/id/layout.json index 8840853cab..0da0c35c24 100644 --- a/clash-verge-rev/src/locales/id/layout.json +++ b/clash-verge-rev/src/locales/id/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/jp/layout.json b/clash-verge-rev/src/locales/jp/layout.json index 8e8a879819..100538d943 100644 --- a/clash-verge-rev/src/locales/jp/layout.json +++ b/clash-verge-rev/src/locales/jp/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/ko/layout.json b/clash-verge-rev/src/locales/ko/layout.json index ffc3ccb27b..10e9f628bf 100644 --- a/clash-verge-rev/src/locales/ko/layout.json +++ b/clash-verge-rev/src/locales/ko/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "메뉴 재정렬 모드", + "restoreDefaultOrder": "Restore default order", "unlock": "메뉴 순서 잠금 해제", "lock": "메뉴 순서 잠금" } diff --git a/clash-verge-rev/src/locales/ru/layout.json b/clash-verge-rev/src/locales/ru/layout.json index ed99badaec..2b59d36e42 100644 --- a/clash-verge-rev/src/locales/ru/layout.json +++ b/clash-verge-rev/src/locales/ru/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/tr/layout.json b/clash-verge-rev/src/locales/tr/layout.json index 839bc6e833..23f2da7bd0 100644 --- a/clash-verge-rev/src/locales/tr/layout.json +++ b/clash-verge-rev/src/locales/tr/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/tt/layout.json b/clash-verge-rev/src/locales/tt/layout.json index 140644bb8e..474dc3eb2d 100644 --- a/clash-verge-rev/src/locales/tt/layout.json +++ b/clash-verge-rev/src/locales/tt/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "Menu reorder mode", + "restoreDefaultOrder": "Restore default order", "unlock": "Unlock menu order", "lock": "Lock menu order" } diff --git a/clash-verge-rev/src/locales/zh/layout.json b/clash-verge-rev/src/locales/zh/layout.json index 4ecfb4c4f7..0a2b2cbc83 100644 --- a/clash-verge-rev/src/locales/zh/layout.json +++ b/clash-verge-rev/src/locales/zh/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "菜单排序模式", + "restoreDefaultOrder": "恢复默认排序", "unlock": "解锁菜单排序", "lock": "锁定菜单排序" } diff --git a/clash-verge-rev/src/locales/zh/proxies.json b/clash-verge-rev/src/locales/zh/proxies.json index e95db1d0ff..e70101bd54 100644 --- a/clash-verge-rev/src/locales/zh/proxies.json +++ b/clash-verge-rev/src/locales/zh/proxies.json @@ -80,10 +80,10 @@ "relay": "根据定义的代理链传递" }, "policies": { - "DIRECT": "直连 (DIRECT)", + "DIRECT": "直接连接 (DIRECT)", "REJECT": "拦截请求 (REJECT)", - "REJECT-DROP": "抛弃请求 (REJECT-DROP)", - "PASS": "跳过此规则 (PASS)" + "REJECT-DROP": "丢弃请求 (REJECT-DROP)", + "PASS": "跳过此项 (PASS)" } } } diff --git a/clash-verge-rev/src/locales/zh/rules.json b/clash-verge-rev/src/locales/zh/rules.json index d660f141c8..52ae91e176 100644 --- a/clash-verge-rev/src/locales/zh/rules.json +++ b/clash-verge-rev/src/locales/zh/rules.json @@ -46,7 +46,7 @@ "DOMAIN-SUFFIX": "匹配域名后缀 (DOMAIN-SUFFIX)", "DOMAIN-KEYWORD": "匹配域名关键字 (DOMAIN-KEYWORD)", "DOMAIN-REGEX": "匹配域名正则表达式 (DOMAIN-REGEX)", - "GEOSITE": "匹配 Geosite 内的域名 (GEOSITE)", + "GEOSITE": "匹配 GeoSite 内的域名 (GEOSITE)", "GEOIP": "匹配 IP 所属国家代码 (GEOIP)", "SRC-GEOIP": "匹配来源 IP 所属国家代码 (SRC-GEOIP)", "IP-ASN": "匹配 IP 所属 ASN (IP-ASN)", diff --git a/clash-verge-rev/src/locales/zhtw/layout.json b/clash-verge-rev/src/locales/zhtw/layout.json index 820056d0e6..635992f631 100644 --- a/clash-verge-rev/src/locales/zhtw/layout.json +++ b/clash-verge-rev/src/locales/zhtw/layout.json @@ -13,6 +13,7 @@ }, "menu": { "reorderMode": "選單排序模式", + "restoreDefaultOrder": "恢復預設排序", "unlock": "解鎖選單排序", "lock": "鎖定選單排序" } diff --git a/clash-verge-rev/src/locales/zhtw/proxies.json b/clash-verge-rev/src/locales/zhtw/proxies.json index f4a1e32c45..48b14e03b2 100644 --- a/clash-verge-rev/src/locales/zhtw/proxies.json +++ b/clash-verge-rev/src/locales/zhtw/proxies.json @@ -80,10 +80,10 @@ "relay": "根據定義的代理鏈傳送" }, "policies": { - "DIRECT": "直連 (DIRECT)", - "REJECT": "拒絕請求 (REJECT)", + "DIRECT": "直接連線 (DIRECT)", + "REJECT": "攔截請求 (REJECT)", "REJECT-DROP": "丟棄請求 (REJECT-DROP)", - "PASS": "跳過此規則 (PASS)" + "PASS": "跳過此項 (PASS)" } } } diff --git a/clash-verge-rev/src/locales/zhtw/rules.json b/clash-verge-rev/src/locales/zhtw/rules.json index 73ca39db77..2fc1ec09c8 100644 --- a/clash-verge-rev/src/locales/zhtw/rules.json +++ b/clash-verge-rev/src/locales/zhtw/rules.json @@ -46,7 +46,7 @@ "DOMAIN-SUFFIX": "配對網域後綴 (DOMAIN-SUFFIX)", "DOMAIN-KEYWORD": "配對網域關鍵字 (DOMAIN-KEYWORD)", "DOMAIN-REGEX": "配對網域正規表示式 (DOMAIN-REGEX)", - "GEOSITE": "配對 Geosite 內的網域 (GEOSITE)", + "GEOSITE": "配對 GeoSite 內的網域 (GEOSITE)", "GEOIP": "配對 IP 所屬國家代碼 (GEOIP)", "SRC-GEOIP": "配對來源 IP 所屬國家代碼 (SRC-GEOIP)", "IP-ASN": "配對 IP 所屬 ASN (IP-ASN)", diff --git a/clash-verge-rev/src/main.tsx b/clash-verge-rev/src/main.tsx index c2b9d42ca7..6680f88125 100644 --- a/clash-verge-rev/src/main.tsx +++ b/clash-verge-rev/src/main.tsx @@ -101,3 +101,9 @@ window.addEventListener("beforeunload", () => { // Clean up all WebSocket instances to prevent memory leaks MihomoWebSocket.cleanupAll(); }); + +// Page loaded event +window.addEventListener("DOMContentLoaded", () => { + // Clean up all WebSocket instances to prevent memory leaks + MihomoWebSocket.cleanupAll(); +}); diff --git a/clash-verge-rev/src/pages/_layout.tsx b/clash-verge-rev/src/pages/_layout.tsx index 73ff968a42..a5ea23ccd1 100644 --- a/clash-verge-rev/src/pages/_layout.tsx +++ b/clash-verge-rev/src/pages/_layout.tsx @@ -1,6 +1,5 @@ import { DndContext, - DragEndEvent, KeyboardSensor, PointerSensor, closestCenter, @@ -9,7 +8,6 @@ import { } from "@dnd-kit/core"; import { SortableContext, - arrayMove, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable"; @@ -25,14 +23,7 @@ import { } from "@mui/material"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { - useCallback, - useEffect, - useMemo, - useReducer, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties } from "react"; import { useTranslation } from "react-i18next"; import { Outlet, useNavigate } from "react-router"; @@ -54,10 +45,13 @@ import { useWindowDecorations } from "@/hooks/use-window"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; -import { handleNoticeMessage } from "./_layout/notificationHandlers"; -import { useAppInitialization } from "./_layout/useAppInitialization"; -import { useLayoutEvents } from "./_layout/useLayoutEvents"; -import { useLoadingOverlay } from "./_layout/useLoadingOverlay"; +import { + useAppInitialization, + useLayoutEvents, + useLoadingOverlay, + useNavMenuOrder, +} from "./_layout/hooks"; +import { handleNoticeMessage } from "./_layout/utils"; import { navItems } from "./_routers"; import "dayjs/locale/ru"; @@ -67,52 +61,7 @@ export const portableFlag = false; type NavItem = (typeof navItems)[number]; -const createNavLookup = (items: NavItem[]) => { - const map = new Map(items.map((item) => [item.path, item])); - const defaultOrder = items.map((item) => item.path); - return { map, defaultOrder }; -}; - -const resolveMenuOrder = ( - order: string[] | null | undefined, - defaultOrder: string[], - map: Map, -) => { - const seen = new Set(); - const resolved: string[] = []; - - if (Array.isArray(order)) { - for (const path of order) { - if (map.has(path) && !seen.has(path)) { - resolved.push(path); - seen.add(path); - } - } - } - - for (const path of defaultOrder) { - if (!seen.has(path)) { - resolved.push(path); - seen.add(path); - } - } - - return resolved; -}; - -const areOrdersEqual = (a: string[], b: string[]) => - a.length === b.length && a.every((value, index) => value === b[index]); - type MenuContextPosition = { top: number; left: number }; -type MenuOrderAction = { type: "sync"; payload: string[] }; - -const menuOrderReducer = (state: string[], action: MenuOrderAction) => { - const next = action.payload; - if (areOrdersEqual(state, next)) { - return state; - } - return [...next]; -}; interface SortableNavMenuItemProps { item: NavItem; @@ -190,69 +139,35 @@ const Layout = () => { }), ); - const { map: navItemMap, defaultOrder: defaultMenuOrder } = useMemo( - () => createNavLookup(navItems), - [], - ); - - const configMenuOrder = useMemo( - () => resolveMenuOrder(verge?.menu_order, defaultMenuOrder, navItemMap), - [verge?.menu_order, defaultMenuOrder, navItemMap], - ); - - const [menuOrder, dispatchMenuOrder] = useReducer( - menuOrderReducer, - configMenuOrder, - ); - - useEffect(() => { - dispatchMenuOrder({ type: "sync", payload: configMenuOrder }); - }, [configMenuOrder]); - - const handleMenuDragEnd = useCallback( - async (event: DragEndEvent) => { - if (!menuUnlocked) { - return; - } - - const { active, over } = event; - if (!over || active.id === over.id) { - return; - } - - const activeId = String(active.id); - const overId = String(over.id); - - const oldIndex = menuOrder.indexOf(activeId); - const newIndex = menuOrder.indexOf(overId); - - if (oldIndex === -1 || newIndex === -1) { - return; - } - - const previousOrder = [...menuOrder]; - const nextOrder = arrayMove(menuOrder, oldIndex, newIndex); - - dispatchMenuOrder({ type: "sync", payload: nextOrder }); + const handleMenuOrderOptimisticUpdate = useCallback( + (order: string[]) => { mutateVerge( - (prev) => (prev ? { ...prev, menu_order: nextOrder } : prev), + (prev) => (prev ? { ...prev, menu_order: order } : prev), false, ); - - try { - await patchVerge({ menu_order: nextOrder }); - } catch (error) { - console.error("Failed to update menu order:", error); - dispatchMenuOrder({ type: "sync", payload: previousOrder }); - mutateVerge( - (prev) => (prev ? { ...prev, menu_order: previousOrder } : prev), - false, - ); - } }, - [menuUnlocked, menuOrder, mutateVerge, patchVerge], + [mutateVerge], ); + const handleMenuOrderPersist = useCallback( + (order: string[]) => patchVerge({ menu_order: order }), + [patchVerge], + ); + + const { + menuOrder, + navItemMap, + handleMenuDragEnd, + isDefaultOrder, + resetMenuOrder, + } = useNavMenuOrder({ + enabled: menuUnlocked, + items: navItems, + storedOrder: verge?.menu_order, + onOptimisticUpdate: handleMenuOrderOptimisticUpdate, + onPersist: handleMenuOrderPersist, + }); + const handleMenuContextMenu = useCallback( (event: React.MouseEvent) => { event.preventDefault(); @@ -266,6 +181,11 @@ const Layout = () => { setMenuContextPosition(null); }, []); + const handleResetMenuOrder = useCallback(() => { + setMenuContextPosition(null); + void resetMenuOrder(); + }, [resetMenuOrder]); + const handleUnlockMenu = useCallback(() => { setMenuUnlocked(true); setMenuContextPosition(null); @@ -518,6 +438,13 @@ const Layout = () => { ? t("layout.components.navigation.menu.lock") : t("layout.components.navigation.menu.unlock")} + + {t("layout.components.navigation.menu.restoreDefaultOrder")} +
diff --git a/clash-verge-rev/src/pages/_layout/hooks/index.ts b/clash-verge-rev/src/pages/_layout/hooks/index.ts new file mode 100644 index 0000000000..c3cc56ef1c --- /dev/null +++ b/clash-verge-rev/src/pages/_layout/hooks/index.ts @@ -0,0 +1,4 @@ +export { useAppInitialization } from "./use-app-initialization"; +export { useLayoutEvents } from "./use-layout-events"; +export { useLoadingOverlay } from "./use-loading-overlay"; +export { useNavMenuOrder } from "./use-nav-menu-order"; diff --git a/clash-verge-rev/src/pages/_layout/useAppInitialization.ts b/clash-verge-rev/src/pages/_layout/hooks/use-app-initialization.ts similarity index 81% rename from clash-verge-rev/src/pages/_layout/useAppInitialization.ts rename to clash-verge-rev/src/pages/_layout/hooks/use-app-initialization.ts index 34abf9f7ec..d791ecb5b5 100644 --- a/clash-verge-rev/src/pages/_layout/useAppInitialization.ts +++ b/clash-verge-rev/src/pages/_layout/hooks/use-app-initialization.ts @@ -1,6 +1,8 @@ import { invoke } from "@tauri-apps/api/core"; import { useEffect, useRef } from "react"; +import { hideInitialOverlay } from "../utils"; + export const useAppInitialization = () => { const initRef = useRef(false); @@ -25,6 +27,8 @@ export const useAppInitialization = () => { }; const notifyBackend = async (stage?: string) => { + if (isCancelled) return; + try { if (stage) { await invoke("update_ui_stage", { stage }); @@ -32,20 +36,16 @@ export const useAppInitialization = () => { await invoke("notify_ui_ready"); } } catch (err) { - console.error(`[初始化] 通知后端失败:`, err); + console.error(`[Initialization] Failed to notify backend:`, err); } }; const removeLoadingOverlay = () => { - const overlay = document.getElementById("initial-loading-overlay"); - if (overlay) { - overlay.style.opacity = "0"; - scheduleTimeout(() => overlay.remove(), 300); - } + hideInitialOverlay({ schedule: scheduleTimeout }); }; const performInitialization = async () => { - if (isInitialized) return; + if (isCancelled || isInitialized) return; isInitialized = true; try { @@ -70,14 +70,18 @@ export const useAppInitialization = () => { await notifyBackend("ResourcesLoaded"); await notifyBackend(); } catch (error) { - console.error("[初始化] 失败:", error); - removeLoadingOverlay(); - notifyBackend().catch(console.error); + if (!isCancelled) { + console.error("[Initialization] Failed:", error); + removeLoadingOverlay(); + notifyBackend().catch(console.error); + } } }; const checkBackendReady = async () => { try { + if (isCancelled) return; + await invoke("update_ui_stage", { stage: "Loading" }); performInitialization(); } catch { @@ -99,7 +103,7 @@ export const useAppInitialization = () => { try { window.clearTimeout(id); } catch (error) { - console.warn("[初始化] 清理定时器失败:", error); + console.warn("[Initialization] Failed to clear timer:", error); } }); timers.clear(); diff --git a/clash-verge-rev/src/pages/_layout/useLayoutEvents.ts b/clash-verge-rev/src/pages/_layout/hooks/use-layout-events.ts similarity index 75% rename from clash-verge-rev/src/pages/_layout/useLayoutEvents.ts rename to clash-verge-rev/src/pages/_layout/hooks/use-layout-events.ts index 0b7955b307..3f71ab34fb 100644 --- a/clash-verge-rev/src/pages/_layout/useLayoutEvents.ts +++ b/clash-verge-rev/src/pages/_layout/hooks/use-layout-events.ts @@ -13,6 +13,10 @@ export const useLayoutEvents = ( useEffect(() => { const unlisteners: Array<() => void> = []; let disposed = false; + const revalidateKeys = (keys: readonly string[]) => { + const keySet = new Set(keys); + mutate((key) => typeof key === "string" && keySet.has(key)); + }; const register = ( maybeUnlisten: void | (() => void) | Promise void)>, @@ -33,25 +37,31 @@ export const useLayoutEvents = ( unlisteners.push(unlisten); } }) - .catch((error) => console.error("[事件监听] 注册失败", error)); + .catch((error) => + console.error("[Event Listener] Registration failed:", error), + ); }; register( addListener("verge://refresh-clash-config", async () => { - mutate("getProxies"); - mutate("getVersion"); - mutate("getClashConfig"); - mutate("getProxyProviders"); + revalidateKeys([ + "getProxies", + "getVersion", + "getClashConfig", + "getProxyProviders", + ]); }), ); register( addListener("verge://refresh-verge-config", () => { - mutate("getVergeConfig"); - mutate("getSystemProxy"); - mutate("getAutotemProxy"); - mutate("getRunningMode"); - mutate("isServiceAvailable"); + revalidateKeys([ + "getVergeConfig", + "getSystemProxy", + "getAutotemProxy", + "getRunningMode", + "isServiceAvailable", + ]); }), ); @@ -91,7 +101,7 @@ export const useLayoutEvents = ( if (errors.length > 0) { console.error( - `[事件监听] 清理过程中发生 ${errors.length} 个错误:`, + `[Event Listener] Encountered ${errors.length} errors during cleanup:`, errors, ); } diff --git a/clash-verge-rev/src/pages/_layout/useLoadingOverlay.ts b/clash-verge-rev/src/pages/_layout/hooks/use-loading-overlay.ts similarity index 52% rename from clash-verge-rev/src/pages/_layout/useLoadingOverlay.ts rename to clash-verge-rev/src/pages/_layout/hooks/use-loading-overlay.ts index d26e138dba..641baf15b4 100644 --- a/clash-verge-rev/src/pages/_layout/useLoadingOverlay.ts +++ b/clash-verge-rev/src/pages/_layout/hooks/use-loading-overlay.ts @@ -1,13 +1,15 @@ import { useEffect, useRef } from "react"; +import { hideInitialOverlay } from "../utils"; + export const useLoadingOverlay = (themeReady: boolean) => { const overlayRemovedRef = useRef(false); useEffect(() => { if (!themeReady || overlayRemovedRef.current) return; - let fadeTimer: number | null = null; - let retryTimer: number | null = null; + let removalTimer: number | undefined; + let retryTimer: number | undefined; let attempts = 0; const maxAttempts = 50; let stopped = false; @@ -15,19 +17,15 @@ export const useLoadingOverlay = (themeReady: boolean) => { const tryRemoveOverlay = () => { if (stopped || overlayRemovedRef.current) return; - const overlay = document.getElementById("initial-loading-overlay"); - if (overlay) { - overlayRemovedRef.current = true; - overlay.style.opacity = "0"; - overlay.style.pointerEvents = "none"; + const { removed, removalTimer: timerId } = hideInitialOverlay({ + assumeMissingAsRemoved: true, + }); + if (typeof timerId === "number") { + removalTimer = timerId; + } - fadeTimer = window.setTimeout(() => { - try { - overlay.remove(); - } catch (error) { - console.warn("[加载遮罩] 移除失败:", error); - } - }, 300); + if (removed) { + overlayRemovedRef.current = true; return; } @@ -35,7 +33,7 @@ export const useLoadingOverlay = (themeReady: boolean) => { attempts += 1; retryTimer = window.setTimeout(tryRemoveOverlay, 100); } else { - console.warn("[加载遮罩] 未找到元素"); + console.warn("[Loading Overlay] Element not found"); } }; @@ -43,8 +41,8 @@ export const useLoadingOverlay = (themeReady: boolean) => { return () => { stopped = true; - if (fadeTimer) window.clearTimeout(fadeTimer); - if (retryTimer) window.clearTimeout(retryTimer); + if (typeof removalTimer === "number") window.clearTimeout(removalTimer); + if (typeof retryTimer === "number") window.clearTimeout(retryTimer); }; }, [themeReady]); }; diff --git a/clash-verge-rev/src/pages/_layout/hooks/use-nav-menu-order.ts b/clash-verge-rev/src/pages/_layout/hooks/use-nav-menu-order.ts new file mode 100644 index 0000000000..cac00ca434 --- /dev/null +++ b/clash-verge-rev/src/pages/_layout/hooks/use-nav-menu-order.ts @@ -0,0 +1,155 @@ +import type { DragEndEvent } from "@dnd-kit/core"; +import { arrayMove } from "@dnd-kit/sortable"; +import { useCallback, useEffect, useMemo, useReducer } from "react"; + +type MenuOrderAction = { type: "sync"; payload: string[] }; + +const areOrdersEqual = (a: string[], b: string[]) => + a.length === b.length && a.every((value, index) => value === b[index]); + +const menuOrderReducer = (state: string[], action: MenuOrderAction) => { + const next = action.payload; + if (areOrdersEqual(state, next)) { + return state; + } + return [...next]; +}; + +const createNavLookup = (items: readonly T[]) => { + const map = new Map(items.map((item) => [item.path, item] as const)); + const defaultOrder = items.map((item) => item.path); + return { map, defaultOrder }; +}; + +const resolveMenuOrder = ( + order: string[] | null | undefined, + defaultOrder: string[], + map: Map, +) => { + const seen = new Set(); + const resolved: string[] = []; + + if (Array.isArray(order)) { + for (const path of order) { + if (map.has(path) && !seen.has(path)) { + resolved.push(path); + seen.add(path); + } + } + } + + for (const path of defaultOrder) { + if (!seen.has(path)) { + resolved.push(path); + seen.add(path); + } + } + + return resolved; +}; + +interface UseNavMenuOrderOptions { + enabled: boolean; + items: readonly T[]; + storedOrder: string[] | null | undefined; + onOptimisticUpdate?: (order: string[]) => void; + onPersist: (order: string[]) => Promise; +} + +export const useNavMenuOrder = ({ + enabled, + items, + storedOrder, + onOptimisticUpdate, + onPersist, +}: UseNavMenuOrderOptions) => { + const { map: navItemMap, defaultOrder } = useMemo( + () => createNavLookup(items), + [items], + ); + + const configMenuOrder = useMemo( + () => resolveMenuOrder(storedOrder, defaultOrder, navItemMap), + [storedOrder, defaultOrder, navItemMap], + ); + + const [menuOrder, dispatchMenuOrder] = useReducer( + menuOrderReducer, + configMenuOrder, + ); + + useEffect(() => { + dispatchMenuOrder({ type: "sync", payload: configMenuOrder }); + }, [configMenuOrder]); + + const isDefaultOrder = useMemo( + () => areOrdersEqual(menuOrder, defaultOrder), + [menuOrder, defaultOrder], + ); + + const handleMenuDragEnd = useCallback( + async (event: DragEndEvent) => { + if (!enabled) { + return; + } + + const { active, over } = event; + if (!over || active.id === over.id) { + return; + } + + const activeId = String(active.id); + const overId = String(over.id); + + const oldIndex = menuOrder.indexOf(activeId); + const newIndex = menuOrder.indexOf(overId); + + if (oldIndex === -1 || newIndex === -1) { + return; + } + + const previousOrder = [...menuOrder]; + const nextOrder = arrayMove(menuOrder, oldIndex, newIndex); + + dispatchMenuOrder({ type: "sync", payload: nextOrder }); + onOptimisticUpdate?.(nextOrder); + + try { + await onPersist(nextOrder); + } catch (error) { + console.error("Failed to update menu order:", error); + dispatchMenuOrder({ type: "sync", payload: previousOrder }); + onOptimisticUpdate?.(previousOrder); + } + }, + [enabled, menuOrder, onOptimisticUpdate, onPersist], + ); + + const resetMenuOrder = useCallback(async () => { + if (isDefaultOrder) { + return; + } + + const previousOrder = [...menuOrder]; + const nextOrder = [...defaultOrder]; + + dispatchMenuOrder({ type: "sync", payload: nextOrder }); + onOptimisticUpdate?.(nextOrder); + + try { + await onPersist(nextOrder); + } catch (error) { + console.error("Failed to reset menu order:", error); + dispatchMenuOrder({ type: "sync", payload: previousOrder }); + onOptimisticUpdate?.(previousOrder); + } + }, [defaultOrder, isDefaultOrder, menuOrder, onOptimisticUpdate, onPersist]); + + return { + menuOrder, + navItemMap, + handleMenuDragEnd, + isDefaultOrder, + resetMenuOrder, + }; +}; diff --git a/clash-verge-rev/src/pages/_layout/useLazyDataLoad.ts b/clash-verge-rev/src/pages/_layout/useLazyDataLoad.ts deleted file mode 100644 index 3eb0434cfa..0000000000 --- a/clash-verge-rev/src/pages/_layout/useLazyDataLoad.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect, useRef } from "react"; - -export const useLazyDataLoad = ( - callbacks: Array<() => void>, - delay: number = 1000, -) => { - const hasLoadedRef = useRef(false); - - useEffect(() => { - if (hasLoadedRef.current) return; - - const timer = window.setTimeout(() => { - hasLoadedRef.current = true; - callbacks.forEach((callback) => { - try { - callback(); - } catch (error) { - console.error("[延迟加载] 执行失败:", error); - } - }); - }, delay); - - return () => window.clearTimeout(timer); - }, [callbacks, delay]); -}; diff --git a/clash-verge-rev/src/pages/_layout/utils/index.ts b/clash-verge-rev/src/pages/_layout/utils/index.ts new file mode 100644 index 0000000000..76dde298cc --- /dev/null +++ b/clash-verge-rev/src/pages/_layout/utils/index.ts @@ -0,0 +1,2 @@ +export { hideInitialOverlay } from "./initial-loading-overlay"; +export { handleNoticeMessage } from "./notification-handlers"; diff --git a/clash-verge-rev/src/pages/_layout/utils/initial-loading-overlay.ts b/clash-verge-rev/src/pages/_layout/utils/initial-loading-overlay.ts new file mode 100644 index 0000000000..e57a145936 --- /dev/null +++ b/clash-verge-rev/src/pages/_layout/utils/initial-loading-overlay.ts @@ -0,0 +1,45 @@ +const OVERLAY_ID = "initial-loading-overlay"; +const REMOVE_DELAY = 300; + +let overlayRemoved = false; + +type HideOverlayOptions = { + schedule?: (handler: () => void, delay: number) => number; + assumeMissingAsRemoved?: boolean; +}; + +type HideOverlayResult = { + removed: boolean; + removalTimer?: number; +}; + +export const hideInitialOverlay = ( + options: HideOverlayOptions = {}, +): HideOverlayResult => { + if (overlayRemoved) { + return { removed: true }; + } + + const overlay = document.getElementById(OVERLAY_ID); + if (!overlay) { + if (options.assumeMissingAsRemoved) { + overlayRemoved = true; + return { removed: true }; + } + return { removed: false }; + } + + overlayRemoved = true; + overlay.dataset.hidden = "true"; + + const schedule = options.schedule ?? window.setTimeout; + const removalTimer = schedule(() => { + try { + overlay.remove(); + } catch (error) { + console.warn("[Loading Overlay] Removal failed:", error); + } + }, REMOVE_DELAY); + + return { removed: true, removalTimer }; +}; diff --git a/clash-verge-rev/src/pages/_layout/notificationHandlers.ts b/clash-verge-rev/src/pages/_layout/utils/notification-handlers.ts similarity index 98% rename from clash-verge-rev/src/pages/_layout/notificationHandlers.ts rename to clash-verge-rev/src/pages/_layout/utils/notification-handlers.ts index 29cda855b2..2b33a2c9ff 100644 --- a/clash-verge-rev/src/pages/_layout/notificationHandlers.ts +++ b/clash-verge-rev/src/pages/_layout/utils/notification-handlers.ts @@ -1,4 +1,4 @@ -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; type NavigateFunction = (path: string, options?: any) => void; type TranslateFunction = (key: string) => string; diff --git a/clash-verge-rev/src/pages/connections.tsx b/clash-verge-rev/src/pages/connections.tsx index 52475813e2..8dcbe84e16 100644 --- a/clash-verge-rev/src/pages/connections.tsx +++ b/clash-verge-rev/src/pages/connections.tsx @@ -28,7 +28,7 @@ import { import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; import { useConnectionData } from "@/hooks/use-connection-data"; -import { useConnectionSetting } from "@/services/states"; +import { useConnectionSetting } from "@/hooks/use-connection-setting"; import parseTraffic from "@/utils/parse-traffic"; type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; @@ -129,8 +129,9 @@ const ConnectionsPage = () => { height: "100%", display: "flex", flexDirection: "column", - overflow: "auto", + overflow: "hidden", borderRadius: "8px", + minHeight: 0, }} header={ @@ -243,6 +244,8 @@ const ConnectionsPage = () => { style={{ flex: 1, borderRadius: "8px", + WebkitOverflowScrolling: "touch", + overscrollBehavior: "contain", }} data={filterConn} itemContent={(_, item) => ( diff --git a/clash-verge-rev/src/pages/logs.tsx b/clash-verge-rev/src/pages/logs.tsx index c3dbb9e31d..53d012cb74 100644 --- a/clash-verge-rev/src/pages/logs.tsx +++ b/clash-verge-rev/src/pages/logs.tsx @@ -13,8 +13,8 @@ import { BaseSearchBox } from "@/components/base/base-search-box"; import { SearchState } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; import LogItem from "@/components/log/log-item"; +import { useClashLog } from "@/hooks/use-clash-log"; import { useLogData } from "@/hooks/use-log-data"; -import { LogFilter, useClashLog } from "@/services/states"; const LogPage = () => { const { t } = useTranslation(); diff --git a/clash-verge-rev/src/pages/profiles.tsx b/clash-verge-rev/src/pages/profiles.tsx index 15109e49c7..03fa43ced2 100644 --- a/clash-verge-rev/src/pages/profiles.tsx +++ b/clash-verge-rev/src/pages/profiles.tsx @@ -58,7 +58,7 @@ import { reorderProfile, updateProfile, } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useSetLoadingCache, useThemeMode } from "@/services/states"; import { debugLog } from "@/utils/debug"; diff --git a/clash-verge-rev/src/pages/rules.tsx b/clash-verge-rev/src/pages/rules.tsx index 266db15d7d..b418e54ad3 100644 --- a/clash-verge-rev/src/pages/rules.tsx +++ b/clash-verge-rev/src/pages/rules.tsx @@ -8,7 +8,7 @@ import { BaseSearchBox } from "@/components/base/base-search-box"; import { ScrollTopButton } from "@/components/layout/scroll-top-button"; import { ProviderButton } from "@/components/rule/provider-button"; import RuleItem from "@/components/rule/rule-item"; -import { useRuleProvidersData, useRulesData } from "@/hooks/app-data"; +import { useRuleProvidersData, useRulesData } from "@/hooks/use-clash-data"; import { useVisibility } from "@/hooks/use-visibility"; const RulesPage = () => { diff --git a/clash-verge-rev/src/pages/settings.tsx b/clash-verge-rev/src/pages/settings.tsx index 448fc35084..56c3655923 100644 --- a/clash-verge-rev/src/pages/settings.tsx +++ b/clash-verge-rev/src/pages/settings.tsx @@ -9,7 +9,7 @@ import SettingSystem from "@/components/setting/setting-system"; import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced"; import SettingVergeBasic from "@/components/setting/setting-verge-basic"; import { openWebUrl } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { useThemeMode } from "@/services/states"; const SettingPage = () => { diff --git a/clash-verge-rev/src/pages/unlock.tsx b/clash-verge-rev/src/pages/unlock.tsx index c856767e5c..8fcf36a9a7 100644 --- a/clash-verge-rev/src/pages/unlock.tsx +++ b/clash-verge-rev/src/pages/unlock.tsx @@ -25,7 +25,7 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseEmpty, BasePage } from "@/components/base"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; interface UnlockItem { name: string; diff --git a/clash-verge-rev/src/providers/window/WindowContext.ts b/clash-verge-rev/src/providers/window/WindowContext.ts index e5f7380e67..1cc0c3ba11 100644 --- a/clash-verge-rev/src/providers/window/WindowContext.ts +++ b/clash-verge-rev/src/providers/window/WindowContext.ts @@ -6,8 +6,8 @@ export interface WindowContextType { maximized: boolean | null; toggleDecorations: () => Promise; refreshDecorated: () => Promise; - minimize: () => void; - close: () => void; + minimize: () => Promise; + close: () => Promise; toggleMaximize: () => Promise; toggleFullscreen: () => Promise; currentWindow: ReturnType; diff --git a/clash-verge-rev/src/providers/window/WindowProvider.tsx b/clash-verge-rev/src/providers/window/WindowProvider.tsx index b43d22dafe..7fb0893ff8 100644 --- a/clash-verge-rev/src/providers/window/WindowProvider.tsx +++ b/clash-verge-rev/src/providers/window/WindowProvider.tsx @@ -12,8 +12,16 @@ export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({ const [decorated, setDecorated] = useState(null); const [maximized, setMaximized] = useState(null); - const close = useCallback(() => currentWindow.close(), [currentWindow]); - const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]); + const close = useCallback(async () => { + // Delay one frame so the UI can clear :hover before the window hides. + await new Promise((resolve) => setTimeout(resolve, 20)); + await currentWindow.close(); + }, [currentWindow]); + const minimize = useCallback(async () => { + // Delay one frame so the UI can clear :hover before the window hides. + await new Promise((resolve) => setTimeout(resolve, 10)); + await currentWindow.minimize(); + }, [currentWindow]); useEffect(() => { let isUnmounted = false; diff --git a/clash-verge-rev/src/services/cmds.ts b/clash-verge-rev/src/services/cmds.ts index 9991eaf17d..1c227acfcb 100644 --- a/clash-verge-rev/src/services/cmds.ts +++ b/clash-verge-rev/src/services/cmds.ts @@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import dayjs from "dayjs"; import { getProxies, getProxyProviders } from "tauri-plugin-mihomo-api"; -import { showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/notice-service"; import { debugLog } from "@/utils/debug"; export async function copyClashEnv() { diff --git a/clash-verge-rev/src/services/noticeService.ts b/clash-verge-rev/src/services/notice-service.ts similarity index 92% rename from clash-verge-rev/src/services/noticeService.ts rename to clash-verge-rev/src/services/notice-service.ts index f0e316c76d..1da7002630 100644 --- a/clash-verge-rev/src/services/noticeService.ts +++ b/clash-verge-rev/src/services/notice-service.ts @@ -311,6 +311,17 @@ const baseShowNotice = ( return id; }; +/** + * Shows a global notice; `showNotice.success / error / info` are the usual entry points. + * + * - `message`: i18n key string, `{ key, params }`, ReactNode, Error/any value (message is extracted) + * - `extras` parsed left-to-right: first plain object is i18n params; next value is raw payload; first number overrides duration (ms, 0 = persistent; defaults: success 3000 / info 5000 / error 8000) + * - Returns a notice id for manual closing via `hideNotice(id)` + * + * @example showNotice.success("profiles.page.feedback.notifications.batchDeleted"); + * @example showNotice.error(err); // pass an Error directly + * @example showNotice.error("profiles.page.feedback.errors.invalidUrl", { url }, 4000); + */ export const showNotice: ShowNotice = Object.assign(baseShowNotice, { success: (message: NoticeContent, ...extras: NoticeExtra[]) => baseShowNotice("success", message, ...extras), diff --git a/clash-verge-rev/src/services/states.ts b/clash-verge-rev/src/services/states.ts index e8a308fd2b..11e28faba8 100644 --- a/clash-verge-rev/src/services/states.ts +++ b/clash-verge-rev/src/services/states.ts @@ -1,50 +1,9 @@ import { createContextState } from "foxact/create-context-state"; -import { useLocalStorage } from "foxact/use-local-storage"; -import { LogLevel } from "tauri-plugin-mihomo-api"; const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState< "light" | "dark" >(); -export type LogFilter = "all" | "debug" | "info" | "warn" | "err"; -export type LogOrder = "asc" | "desc"; - -interface IClashLog { - enable: boolean; - logLevel: LogLevel; - logFilter: LogFilter; - logOrder: LogOrder; -} -const defaultClashLog: IClashLog = { - enable: true, - logLevel: "info", - logFilter: "all", - logOrder: "asc", -}; -export const useClashLog = () => - useLocalStorage("clash-log", defaultClashLog, { - serializer: JSON.stringify, - deserializer: JSON.parse, - }); - -// export const useEnableLog = () => useLocalStorage("enable-log", false); - -interface IConnectionSetting { - layout: "table" | "list"; -} - -const defaultConnectionSetting: IConnectionSetting = { layout: "table" }; - -export const useConnectionSetting = () => - useLocalStorage( - "connections-setting", - defaultConnectionSetting, - { - serializer: JSON.stringify, - deserializer: JSON.parse, - }, - ); - // save the state of each profile item loading const [LoadingCacheProvider, useLoadingCache, useSetLoadingCache] = createContextState>({}); diff --git a/clash-verge-rev/src/services/traffic-monitor-worker.ts b/clash-verge-rev/src/services/traffic-monitor-worker.ts index f88462f294..dcc0964669 100644 --- a/clash-verge-rev/src/services/traffic-monitor-worker.ts +++ b/clash-verge-rev/src/services/traffic-monitor-worker.ts @@ -1,9 +1,3 @@ -import type { - ISamplingConfig, - ITrafficDataPoint, - ITrafficWorkerSnapshotMessage, - TrafficWorkerRequestMessage, -} from "../types/traffic-monitor"; import { TrafficDataSampler, formatTrafficName, diff --git a/clash-verge-rev/src/types/generated/i18n-keys.ts b/clash-verge-rev/src/types/generated/i18n-keys.ts index 822e7dd5b3..4d0fdad7f8 100644 --- a/clash-verge-rev/src/types/generated/i18n-keys.ts +++ b/clash-verge-rev/src/types/generated/i18n-keys.ts @@ -105,6 +105,7 @@ export const translationKeys = [ "layout.components.navigation.tabs.unlock", "layout.components.navigation.tabs.settings", "layout.components.navigation.menu.reorderMode", + "layout.components.navigation.menu.restoreDefaultOrder", "layout.components.navigation.menu.unlock", "layout.components.navigation.menu.lock", "logs.page.title", diff --git a/clash-verge-rev/src/types/generated/i18n-resources.ts b/clash-verge-rev/src/types/generated/i18n-resources.ts index 5f079697b3..2700b138c7 100644 --- a/clash-verge-rev/src/types/generated/i18n-resources.ts +++ b/clash-verge-rev/src/types/generated/i18n-resources.ts @@ -181,6 +181,7 @@ export interface TranslationResources { menu: { lock: string; reorderMode: string; + restoreDefaultOrder: string; unlock: string; }; tabs: { diff --git a/clash-verge-rev/src/types/types.d.ts b/clash-verge-rev/src/types/global.d.ts similarity index 90% rename from clash-verge-rev/src/types/types.d.ts rename to clash-verge-rev/src/types/global.d.ts index fd57faa66f..fd1c5ddc73 100644 --- a/clash-verge-rev/src/types/types.d.ts +++ b/clash-verge-rev/src/types/global.d.ts @@ -197,6 +197,17 @@ interface ILogItem { payload: string; } +type LogLevel = import("tauri-plugin-mihomo-api").LogLevel; +type LogFilter = "all" | "debug" | "info" | "warn" | "err"; +type LogOrder = "asc" | "desc"; + +interface IClashLog { + enable: boolean; + logLevel: LogLevel; + logFilter: LogFilter; + logOrder: LogOrder; +} + interface IConnectionsItem { id: string; metadata: { @@ -227,6 +238,10 @@ interface IConnections { connections: IConnectionsItem[]; } +interface IConnectionSetting { + layout: "table" | "list"; +} + /** * Some interface for command */ @@ -891,3 +906,86 @@ interface IWebDavConfig { username: string; password: string; } + +// Traffic monitor types +interface ITrafficDataPoint { + up: number; + down: number; + timestamp: number; + name: string; +} + +interface ISamplingConfig { + rawDataMinutes: number; + compressedDataMinutes: number; + compressionRatio: number; +} + +interface ISamplerStats { + rawBufferSize: number; + compressedBufferSize: number; + compressionQueueSize: number; + totalMemoryPoints: number; +} + +interface ITrafficWorkerInitMessage { + type: "init"; + config: ISamplingConfig & { + snapshotIntervalMs: number; + defaultRangeMinutes: number; + }; +} + +interface ITrafficWorkerAppendMessage { + type: "append"; + payload: { + up: number; + down: number; + timestamp?: number; + }; +} + +interface ITrafficWorkerClearMessage { + type: "clear"; +} + +interface ITrafficWorkerSetRangeMessage { + type: "setRange"; + minutes: number; +} + +interface ITrafficWorkerRequestSnapshotMessage { + type: "requestSnapshot"; +} + +type TrafficWorkerRequestMessage = + | ITrafficWorkerInitMessage + | ITrafficWorkerAppendMessage + | ITrafficWorkerClearMessage + | ITrafficWorkerSetRangeMessage + | ITrafficWorkerRequestSnapshotMessage; + +interface ITrafficWorkerSnapshotMessage { + type: "snapshot"; + dataPoints: ITrafficDataPoint[]; + availableDataPoints: ITrafficDataPoint[]; + samplerStats: ISamplerStats; + rangeMinutes: number; + lastTimestamp?: number; + reason: + | "init" + | "interval" + | "range-change" + | "request" + | "append-throttle" + | "clear"; +} + +interface ITrafficWorkerLogMessage { + type: "log"; + message: string; +} + +type TrafficWorkerResponseMessage = + | ITrafficWorkerSnapshotMessage + | ITrafficWorkerLogMessage; diff --git a/clash-verge-rev/src/types/traffic-monitor.ts b/clash-verge-rev/src/types/traffic-monitor.ts deleted file mode 100644 index d0b1045177..0000000000 --- a/clash-verge-rev/src/types/traffic-monitor.ts +++ /dev/null @@ -1,81 +0,0 @@ -export interface ITrafficDataPoint { - up: number; - down: number; - timestamp: number; - name: string; -} - -export interface ISamplingConfig { - rawDataMinutes: number; - compressedDataMinutes: number; - compressionRatio: number; -} - -export interface ISamplerStats { - rawBufferSize: number; - compressedBufferSize: number; - compressionQueueSize: number; - totalMemoryPoints: number; -} - -export interface ITrafficWorkerInitMessage { - type: "init"; - config: ISamplingConfig & { - snapshotIntervalMs: number; - defaultRangeMinutes: number; - }; -} - -export interface ITrafficWorkerAppendMessage { - type: "append"; - payload: { - up: number; - down: number; - timestamp?: number; - }; -} - -export interface ITrafficWorkerClearMessage { - type: "clear"; -} - -export interface ITrafficWorkerSetRangeMessage { - type: "setRange"; - minutes: number; -} - -export interface ITrafficWorkerRequestSnapshotMessage { - type: "requestSnapshot"; -} - -export type TrafficWorkerRequestMessage = - | ITrafficWorkerInitMessage - | ITrafficWorkerAppendMessage - | ITrafficWorkerClearMessage - | ITrafficWorkerSetRangeMessage - | ITrafficWorkerRequestSnapshotMessage; - -export interface ITrafficWorkerSnapshotMessage { - type: "snapshot"; - dataPoints: ITrafficDataPoint[]; - availableDataPoints: ITrafficDataPoint[]; - samplerStats: ISamplerStats; - rangeMinutes: number; - lastTimestamp?: number; - reason: - | "init" - | "interval" - | "range-change" - | "request" - | "append-throttle" - | "clear"; -} - -export interface ITrafficWorkerLogMessage { - type: "log"; - message: string; -} - -export type TrafficWorkerResponseMessage = - | ITrafficWorkerSnapshotMessage - | ITrafficWorkerLogMessage; diff --git a/clash-verge-rev/src/utils/debug.ts b/clash-verge-rev/src/utils/debug.ts index e571312398..270b2e992e 100644 --- a/clash-verge-rev/src/utils/debug.ts +++ b/clash-verge-rev/src/utils/debug.ts @@ -1,54 +1,49 @@ -const envVarValue = (import.meta.env.VITE_ENABLE_DEBUG_LOGS ?? "").toString(); +/** + * Debug logging is enabled when: + * - dev build (`import.meta.env.DEV`) + * - env flag `VITE_ENABLE_DEBUG_LOGS` is truthy (1/true/yes) + * - page sets `window.__VERGE_ENABLE_DEBUG_LOGS__ = true` + * - localStorage item `VERGE_DEBUG_LOGS` is truthy (1/true/yes) + * Use `setDebugLoggingEnabled` to force-enable/disable at runtime. + */ +let runtimeOverride: boolean | undefined; +let cachedDebugEnabled: boolean | undefined; -let runtimeOverride: boolean | null = null; - -const parseStringFlag = (value: string) => { - if (!value) return false; - const normalized = value.trim().toLowerCase(); +const parseStringFlag = (value: unknown) => { + const normalized = String(value ?? "") + .trim() + .toLowerCase(); + if (!normalized) return false; return normalized === "1" || normalized === "true" || normalized === "yes"; }; -let cachedDebugEnabled: boolean | null = null; - -const computeDebugEnabled = () => { - if (import.meta.env.DEV) { - return true; - } - - if (parseStringFlag(envVarValue)) { - return true; - } - - if (typeof window !== "undefined") { - const globalFlag = (window as any).__VERGE_ENABLE_DEBUG_LOGS__; - if (typeof globalFlag === "boolean") { - return globalFlag; - } - - try { - const stored = window.localStorage?.getItem("VERGE_DEBUG_LOGS"); - if (stored) { - return parseStringFlag(stored); - } - } catch { - // ignore storage access errors - } - } - - return false; +const readGlobalFlag = (): boolean | null => { + if (typeof window === "undefined") return null; + const flag = (window as any).__VERGE_ENABLE_DEBUG_LOGS__; + return typeof flag === "boolean" ? flag : null; }; -const isEnvDebugEnabled = () => { - if (runtimeOverride !== null) { - return runtimeOverride; +const readStoredFlag = (): boolean | null => { + if (typeof window === "undefined") return null; + try { + const stored = window.localStorage?.getItem("VERGE_DEBUG_LOGS"); + return stored ? parseStringFlag(stored) : null; + } catch { + return null; } +}; - if (cachedDebugEnabled !== null) { - return cachedDebugEnabled; - } +const computeDebugEnabled = (): boolean => { + if (import.meta.env.DEV) return true; + if (parseStringFlag(import.meta.env.VITE_ENABLE_DEBUG_LOGS)) return true; - cachedDebugEnabled = computeDebugEnabled(); - return cachedDebugEnabled; + const globalFlag = readGlobalFlag(); + if (globalFlag !== null) return globalFlag; + + const storedFlag = readStoredFlag(); + if (storedFlag !== null) return storedFlag; + + return false; }; export const setDebugLoggingEnabled = (enabled: boolean) => { @@ -56,9 +51,16 @@ export const setDebugLoggingEnabled = (enabled: boolean) => { cachedDebugEnabled = enabled; }; -export const isDebugLoggingEnabled = () => isEnvDebugEnabled(); +export const isDebugLoggingEnabled = () => + runtimeOverride ?? + cachedDebugEnabled ?? + (cachedDebugEnabled = computeDebugEnabled()); +/** + * Logs to the console only when debug logging is enabled. + * Forwards all arguments to `console.log`; does nothing otherwise. + */ export const debugLog = (...args: any[]) => { - if (!isEnvDebugEnabled()) return; + if (!isDebugLoggingEnabled()) return; console.log(...args); }; diff --git a/clash-verge-rev/src/utils/parse-hotkey.ts b/clash-verge-rev/src/utils/parse-hotkey.ts index 4335a9724c..ddc275d2c8 100644 --- a/clash-verge-rev/src/utils/parse-hotkey.ts +++ b/clash-verge-rev/src/utils/parse-hotkey.ts @@ -1,53 +1,9 @@ import { KeyboardEvent } from "react"; -import { debugLog } from "@/utils/debug"; +import getSystem from "./get-system"; -const KEY_MAP: Record = { - // Option + 特殊字符映射 - "–": "Minus", // Option + - - "≠": "Equal", // Option + = - "\u201C": "BracketLeft", // Option + [ - "\u2019": "BracketRight", // Option + ] - "«": "Backslash", // Option + \ - "…": "Semicolon", // Option + ; - æ: "Quote", // Option + ' - "≤": "Comma", // Option + , - "≥": "Period", // Option + . - "÷": "Slash", // Option + / +const OS = getSystem(); - // Option组合键映射 - Å: "A", - "∫": "B", - Ç: "C", - "∂": "D", - "´": "E", - ƒ: "F", - "©": "G", - "˙": "H", - ˆ: "I", - "∆": "J", - "˚": "K", - "¬": "L", - µ: "M", - "˜": "N", - Ø: "O", - π: "P", - Œ: "Q", - "®": "R", - ß: "S", - "†": "T", - "¨": "U", - "√": "V", - "∑": "W", - "≈": "X", - "¥": "Y", - Ω: "Z", -}; - -const mapKeyCombination = (key: string): string => { - const mappedKey = KEY_MAP[key] || key; - return `${mappedKey}`; -}; export const parseHotkey = (keyEvent: KeyboardEvent) => { const nativeEvent = keyEvent.nativeEvent; const key = nativeEvent.code; @@ -64,16 +20,21 @@ export const parseHotkey = (keyEvent: KeyboardEvent) => { } else if (temp.endsWith("RIGHT")) { temp = temp.slice(0, -5); } - debugLog(temp, mapKeyCombination(temp)); switch (temp) { case "CONTROL": return "CTRL"; + case "ALT": + if (OS === "macos") { + return "OPTION"; + } else { + return "ALT"; + } case "META": return "CMD"; case " ": return "SPACE"; default: - return KEY_MAP[temp] || temp; + return temp; } }; diff --git a/clash-verge-rev/src/utils/parse-traffic.ts b/clash-verge-rev/src/utils/parse-traffic.ts index 514d24fe2d..c884262a27 100644 --- a/clash-verge-rev/src/utils/parse-traffic.ts +++ b/clash-verge-rev/src/utils/parse-traffic.ts @@ -2,8 +2,8 @@ const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const parseTraffic = (num?: number) => { if (typeof num !== "number") return ["NaN", ""]; - if (num < 1000) return [`${Math.round(num)}`, "B"]; - const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1); + const exp = + num < 1 ? 0 : Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1); const dat = num / Math.pow(1024, exp); const ret = dat >= 1000 ? dat.toFixed(0) : dat.toPrecision(3); const unit = UNITS[exp]; diff --git a/clash-verge-rev/src/utils/traffic-sampler.ts b/clash-verge-rev/src/utils/traffic-sampler.ts index a6eaba967f..9b6ba90e82 100644 --- a/clash-verge-rev/src/utils/traffic-sampler.ts +++ b/clash-verge-rev/src/utils/traffic-sampler.ts @@ -1,9 +1,3 @@ -import type { - ISamplerStats, - ISamplingConfig, - ITrafficDataPoint, -} from "@/types/traffic-monitor"; - interface ICompressedDataPoint { up: number; down: number; diff --git a/clash-verge-rev/src/utils/uri-parser.ts b/clash-verge-rev/src/utils/uri-parser.ts index c3a622c5fd..6c0abfd26a 100644 --- a/clash-verge-rev/src/utils/uri-parser.ts +++ b/clash-verge-rev/src/utils/uri-parser.ts @@ -1,37 +1,67 @@ -export default function parseUri(uri: string): IProxyConfig { - const head = uri.split("://")[0]; - switch (head) { - case "ss": - return URI_SS(uri); - case "ssr": - return URI_SSR(uri); - case "vmess": - return URI_VMESS(uri); - case "vless": - return URI_VLESS(uri); - case "trojan": - return URI_Trojan(uri); - case "hysteria2": - return URI_Hysteria2(uri); - case "hy2": - return URI_Hysteria2(uri); - case "hysteria": - return URI_Hysteria(uri); - case "hy": - return URI_Hysteria(uri); - case "tuic": - return URI_TUIC(uri); - case "wireguard": - return URI_Wireguard(uri); - case "wg": - return URI_Wireguard(uri); - case "http": - return URI_HTTP(uri); - case "socks5": - return URI_SOCKS(uri); - default: - throw Error(`Unknown uri type: ${head}`); +type UriParser = (uri: string) => IProxyConfig; + +const URI_SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//; + +const URI_PARSERS: Record = { + ss: URI_SS, + ssr: URI_SSR, + vmess: URI_VMESS, + vless: URI_VLESS, + trojan: URI_Trojan, + hysteria2: URI_Hysteria2, + hy2: URI_Hysteria2, + hysteria: URI_Hysteria, + hy: URI_Hysteria, + tuic: URI_TUIC, + wireguard: URI_Wireguard, + wg: URI_Wireguard, + http: URI_HTTP, + https: URI_HTTP, + socks5: URI_SOCKS, + socks: URI_SOCKS, +}; + +function normalizeUriAndGetScheme(input: string): { + uri: string; + scheme: string; +} { + const trimmed = input.trim(); + const match = URI_SCHEME_RE.exec(trimmed); + if (!match) { + const schemeGuess = (trimmed.split("://")[0] ?? "").toLowerCase(); + return { uri: trimmed, scheme: schemeGuess }; } + + const scheme = match[1].toLowerCase(); + return { uri: scheme + trimmed.slice(match[1].length), scheme }; +} + +function stripUriScheme( + uri: string, + expectedSchemes: string | readonly string[], + errorMessage: string, +): string { + const match = URI_SCHEME_RE.exec(uri); + if (!match) { + throw new Error(errorMessage); + } + const scheme = match[1].toLowerCase(); + + const expected = + typeof expectedSchemes === "string" ? [expectedSchemes] : expectedSchemes; + if (!expected.includes(scheme)) { + throw new Error(errorMessage); + } + return uri.slice(match[0].length); +} + +export default function parseUri(uri: string): IProxyConfig { + const { uri: normalized, scheme } = normalizeUriAndGetScheme(uri); + const parser = URI_PARSERS[scheme]; + if (!parser) { + throw new Error(`Unknown uri type: ${scheme}`); + } + return parser(normalized); } function getIfNotBlank( @@ -41,8 +71,8 @@ function getIfNotBlank( return value && value.trim() !== "" ? value : dft; } -function getIfPresent(value: any, dft?: any): any { - return value ? value : dft; +function getIfPresent(value: T | null | undefined, dft?: T): T | undefined { + return value !== null && value !== undefined ? value : dft; } function isPresent(value: any): boolean { @@ -53,6 +83,182 @@ function trimStr(str: string | undefined): string | undefined { return str ? str.trim() : str; } +function safeDecodeURIComponent(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function decodeAndTrim(value: string | undefined): string | undefined { + const decoded = safeDecodeURIComponent(value); + const trimmed = decoded?.trim(); + return trimmed ? trimmed : undefined; +} + +function splitOnce(input: string, delimiter: string): [string, string?] { + const idx = input.indexOf(delimiter); + if (idx === -1) return [input]; + return [input.slice(0, idx), input.slice(idx + delimiter.length)]; +} + +function parseQueryString( + query: string | undefined, +): Record { + const out: Record = {}; + if (!query) return out; + for (const part of query.split("&")) { + if (!part) continue; + const [keyRaw, valueRaw] = splitOnce(part, "="); + const key = keyRaw.trim(); + if (!key) continue; + out[key] = + valueRaw === undefined + ? undefined + : (safeDecodeURIComponent(valueRaw) ?? valueRaw); + } + return out; +} + +function normalizeQueryKey(key: string): string { + return key.replace(/_/g, "-"); +} + +function parseQueryStringNormalized( + query: string | undefined, +): Record { + const raw = parseQueryString(query); + const normalized: Record = {}; + for (const [key, value] of Object.entries(raw)) { + normalized[normalizeQueryKey(key)] = value; + } + return normalized; +} + +function parseBool(value: string | undefined): boolean | undefined { + if (value === undefined) return undefined; + return /^(?:true|1)$/i.test(value); +} + +function parseBoolOrPresence(value: string | undefined): boolean { + if (value === undefined) return true; + const trimmed = value.trim(); + if (trimmed === "") return true; + return /^(?:true|1)$/i.test(trimmed); +} + +function parseVlessFlow(value: string | undefined): string | undefined { + const flow = getIfNotBlank(value); + if (!flow) return undefined; + if (/^none$/i.test(flow)) return undefined; + if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(flow)) return undefined; + return flow; +} + +function parseInteger(value: string | undefined): number | undefined { + if (value === undefined) return undefined; + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? undefined : parsed; +} + +function parsePortStrict( + value: string | number | null | undefined, +): number | undefined { + if (value === null || value === undefined) return undefined; + const raw = String(value).trim(); + if (!/^\d+$/.test(raw)) return undefined; + const parsed = Number.parseInt(raw, 10); + if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) { + return undefined; + } + return parsed; +} + +function parseRequiredPort( + value: string | number | null | undefined, + errorMessage: string, +): number { + const parsed = parsePortStrict(value); + if (parsed === undefined) { + throw new Error(errorMessage); + } + return parsed; +} + +function parsePortOrDefault(port: string | undefined, dft: number): number { + return parseInteger(port) ?? dft; +} + +const IP_VERSIONS = [ + "dual", + "ipv4", + "ipv6", + "ipv4-prefer", + "ipv6-prefer", +] as const; + +function parseIpVersion( + value: string | undefined, +): (typeof IP_VERSIONS)[number] { + return value && IP_VERSIONS.includes(value as (typeof IP_VERSIONS)[number]) + ? (value as (typeof IP_VERSIONS)[number]) + : "dual"; +} + +type UrlLikeParts = { + auth?: string; + host: string; + port?: string; + query?: string; + fragment?: string; +}; + +const URLLIKE_RE = + /^(?:(?.*?)@)?(?.*?)(?::(?\d+))?\/?(?:\?(?.*?))?(?:#(?.*?))?$/; + +function parseUrlLike( + input: string, + options: { requireAuth: true; errorMessage: string }, +): UrlLikeParts & { auth: string }; +function parseUrlLike( + input: string, + options: { requireAuth?: false; errorMessage: string }, +): UrlLikeParts; +function parseUrlLike( + input: string, + options: { requireAuth?: boolean; errorMessage: string }, +): UrlLikeParts { + const match = URLLIKE_RE.exec(input); + const groups = (match?.groups ?? {}) as { + auth?: string; + host?: string; + port?: string; + query?: string; + fragment?: string; + }; + if (!match || groups.host === undefined) { + throw new Error(options.errorMessage); + } + + const auth = getIfNotBlank(groups.auth); + if (options.requireAuth && !auth) { + throw new Error(options.errorMessage); + } + + const result: UrlLikeParts = { + auth, + host: groups.host, + port: groups.port, + query: groups.query, + fragment: groups.fragment, + }; + return options.requireAuth + ? ({ ...result, auth } as UrlLikeParts & { auth: string }) + : result; +} + function isIPv4(address: string): boolean { // Check if the address is IPv4 const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; @@ -67,188 +273,308 @@ function isIPv6(address: string): boolean { } function decodeBase64OrOriginal(str: string): string { + const normalized = str + .replace(/[\r\n\s]/g, "") + .replace(/-/g, "+") + .replace(/_/g, "/"); + + const padLen = normalized.length % 4; + const padded = + padLen === 0 ? normalized : normalized + "=".repeat(4 - padLen); + try { - return atob(str); + const decoded = atob(padded); + // Heuristic: only accept "text-like" results to avoid accidentally decoding + // non-base64 strings that happen to be decodable. + for (let i = 0; i < decoded.length; i++) { + const code = decoded.charCodeAt(i); + if (code === 9 || code === 10 || code === 13) continue; + if (code < 32 || code === 127) { + return str; + } + } + return decoded; } catch { return str; } } -function getCipher(str: string | undefined) { - switch (str) { - case "none": - return "none"; - case "auto": - return "auto"; - case "dummy": - return "dummy"; - case "aes-128-gcm": - return "aes-128-gcm"; - case "aes-192-gcm": - return "aes-192-gcm"; - case "aes-256-gcm": - return "aes-256-gcm"; - case "lea-128-gcm": - return "lea-128-gcm"; - case "lea-192-gcm": - return "lea-192-gcm"; - case "lea-256-gcm": - return "lea-256-gcm"; - case "aes-128-gcm-siv": - return "aes-128-gcm-siv"; - case "aes-256-gcm-siv": - return "aes-256-gcm-siv"; - case "2022-blake3-aes-128-gcm": - return "2022-blake3-aes-128-gcm"; - case "2022-blake3-aes-256-gcm": - return "2022-blake3-aes-256-gcm"; - case "aes-128-cfb": - return "aes-128-cfb"; - case "aes-192-cfb": - return "aes-192-cfb"; - case "aes-256-cfb": - return "aes-256-cfb"; - case "aes-128-ctr": - return "aes-128-ctr"; - case "aes-192-ctr": - return "aes-192-ctr"; - case "aes-256-ctr": - return "aes-256-ctr"; - case "chacha20": - return "chacha20"; - case "chacha20-poly1305": - return "chacha20-ietf-poly1305"; - case "chacha20-ietf": - return "chacha20-ietf"; - case "chacha20-ietf-poly1305": - return "chacha20-ietf-poly1305"; - case "2022-blake3-chacha20-poly1305": - return "2022-blake3-chacha20-poly1305"; - case "rabbit128-poly1305": - return "rabbit128-poly1305"; - case "xchacha20-ietf-poly1305": - return "xchacha20-ietf-poly1305"; - case "xchacha20": - return "xchacha20"; - case "aegis-128l": - return "aegis-128l"; - case "aegis-256": - return "aegis-256"; - case "aez-384": - return "aez-384"; - case "deoxys-ii-256-128": - return "deoxys-ii-256-128"; - case "rc4-md5": - return "rc4-md5"; - case undefined: - return "none"; - default: - return "auto"; +const CIPHER_ALIASES: Record = { + "chacha20-poly1305": "chacha20-ietf-poly1305", +}; + +const KNOWN_CIPHERS = new Set([ + "none", + "auto", + "dummy", + "aes-128-gcm", + "aes-192-gcm", + "aes-256-gcm", + "lea-128-gcm", + "lea-192-gcm", + "lea-256-gcm", + "aes-128-gcm-siv", + "aes-256-gcm-siv", + "2022-blake3-aes-128-gcm", + "2022-blake3-aes-256-gcm", + "aes-128-cfb", + "aes-192-cfb", + "aes-256-cfb", + "aes-128-ctr", + "aes-192-ctr", + "aes-256-ctr", + "chacha20", + "chacha20-ietf", + "chacha20-ietf-poly1305", + "2022-blake3-chacha20-poly1305", + "rabbit128-poly1305", + "xchacha20-ietf-poly1305", + "xchacha20", + "aegis-128l", + "aegis-256", + "aez-384", + "deoxys-ii-256-128", + "rc4-md5", +]); + +function getCipher(value: unknown): CipherType { + if (value === undefined) return "none"; + if (typeof value !== "string") return "auto"; + const aliased = CIPHER_ALIASES[value] ?? value; + return KNOWN_CIPHERS.has(aliased as CipherType) + ? (aliased as CipherType) + : "auto"; +} + +function firstString(value: any): string | undefined { + if (value === null || value === undefined) return undefined; + if (Array.isArray(value)) { + if (value.length === 0) return undefined; + const first = value[0]; + return first === null || first === undefined ? undefined : String(first); + } + return String(value); +} + +function parseVmessShadowrocketParams(raw: string): Record { + const match = /(^[^?]+?)\/?\?(.*)$/.exec(raw); + if (!match) return {}; + + const [, base64Line, qs] = match; + const content = decodeBase64OrOriginal(base64Line); + const params: Record = {}; + + for (const addon of qs.split("&")) { + if (!addon) continue; + const [keyRaw, valueRaw] = splitOnce(addon, "="); + const key = keyRaw.trim(); + if (!key) continue; + if (valueRaw === undefined) { + params[key] = true; + continue; + } + const value = safeDecodeURIComponent(valueRaw) ?? valueRaw; + params[key] = value.includes(",") ? value.split(",") : value; + } + + const contentMatch = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content); + if (!contentMatch) return params; + + const [, cipher, uuid, server, port] = contentMatch; + params.scy = cipher; + params.id = uuid; + params.port = port; + params.add = server; + return params; +} + +function parseVmessParams(decoded: string, raw: string): Record { + try { + // V2rayN URI format + return JSON.parse(decoded); + } catch (e) { + // Shadowrocket URI format + console.warn( + "[URI_VMESS] JSON.parse(content) failed, falling back to Shadowrocket parsing:", + e, + ); + return parseVmessShadowrocketParams(raw); } } +function parseVmessQuantumult(content: string): IProxyVmessConfig { + const partitions = content.split(",").map((p) => p.trim()); + const params: Record = {}; + for (const part of partitions) { + if (part.indexOf("=") !== -1) { + const [key, val] = splitOnce(part, "="); + params[key.trim()] = val?.trim() ?? ""; + } + } + + const proxy: IProxyVmessConfig = { + name: partitions[0].split("=")[0].trim(), + type: "vmess", + server: partitions[1], + port: parseRequiredPort(partitions[2], "Invalid vmess uri: invalid port"), + cipher: getCipher(getIfNotBlank(partitions[3], "auto")), + uuid: partitions[4].match(/^"(.*)"$/)?.[1] || "", + tls: params.obfs === "wss", + udp: parseBool(params["udp-relay"]), + tfo: parseBool(params["fast-open"]), + "skip-cert-verify": + params["tls-verification"] === undefined + ? undefined + : !parseBool(params["tls-verification"]), + }; + + if (isPresent(params.obfs)) { + if (params.obfs === "ws" || params.obfs === "wss") { + proxy.network = "ws"; + proxy["ws-opts"] = { + path: + (getIfNotBlank(params["obfs-path"]) || '"/"').match( + /^"(.*)"$/, + )?.[1] || "/", + headers: { + Host: + params["obfs-header"]?.match(/Host:\s*([a-zA-Z0-9-.]*)/)?.[1] || "", + }, + }; + } else { + throw new Error(`Unsupported obfs: ${params.obfs}`); + } + } + + return proxy; +} + function URI_SS(line: string): IProxyShadowsocksConfig { - // parse url - let content = line.split("ss://")[1]; + const afterScheme = stripUriScheme(line, "ss", "Invalid ss uri"); + if (!afterScheme) { + throw new Error("Invalid ss uri"); + } + + const [withoutHash, hashRaw] = splitOnce(afterScheme, "#"); + const nameFromHash = decodeAndTrim(hashRaw); + + const [mainRaw, queryRaw] = splitOnce(withoutHash, "?"); + const queryParams = parseQueryString(queryRaw); + + const main = mainRaw.includes("@") + ? mainRaw + : decodeBase64OrOriginal(mainRaw); + const atIdx = main.lastIndexOf("@"); + if (atIdx === -1) { + throw new Error("Invalid ss uri: missing '@'"); + } + + const userInfoStr = decodeBase64OrOriginal(main.slice(0, atIdx)); + const serverAndPortWithPath = main.slice(atIdx + 1); + const serverAndPort = serverAndPortWithPath.split("/")[0]; + + const portIdx = serverAndPort.lastIndexOf(":"); + if (portIdx === -1) { + throw new Error("Invalid ss uri: missing port"); + } + const server = serverAndPort.slice(0, portIdx); + const portRaw = serverAndPort.slice(portIdx + 1); + const port = parseRequiredPort(portRaw, "Invalid ss uri: invalid port"); + + const userInfo = userInfoStr.match(/(^.*?):(.*$)/); const proxy: IProxyShadowsocksConfig = { - name: decodeURIComponent(line.split("#")[1]).trim(), + name: nameFromHash ?? `SS ${server}:${port}`, type: "ss", - server: "", - port: 0, + server, + port, + cipher: getCipher(userInfo?.[1]), + password: userInfo?.[2], }; - content = content.split("#")[0]; // strip proxy name - // handle IPV4 and IPV6 - let serverAndPortArray = content.match(/@([^/]*)(\/|$)/); - let userInfoStr = decodeBase64OrOriginal(content.split("@")[0]); - let query = ""; - if (!serverAndPortArray) { - if (content.includes("?")) { - const parsed = content.match(/^(.*)(\?.*)$/); - content = parsed?.[1] ?? ""; - query = parsed?.[2] ?? ""; - } - content = decodeBase64OrOriginal(content); - if (query) { - if (/(&|\?)v2ray-plugin=/.test(query)) { - const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/); - const v2rayPlugin = parsed![2]; - if (v2rayPlugin) { - proxy.plugin = "v2ray-plugin"; - proxy["plugin-opts"] = JSON.parse( - decodeBase64OrOriginal(v2rayPlugin), - ); - } - } - content = `${content}${query}`; - } - userInfoStr = content.split("@")[0]; - serverAndPortArray = content.match(/@([^/]*)(\/|$)/); - } - const serverAndPort = serverAndPortArray?.[1]; - const portIdx = serverAndPort?.lastIndexOf(":") ?? 0; - proxy.server = serverAndPort?.substring(0, portIdx) ?? ""; - proxy.port = parseInt( - `${serverAndPort?.substring(portIdx + 1)}`.match(/\d+/)?.[0] ?? "", - ); - const userInfo = userInfoStr.match(/(^.*?):(.*$)/); - proxy.cipher = getCipher(userInfo?.[1]); - proxy.password = userInfo?.[2]; - // handle obfs - const idx = content.indexOf("?plugin="); - if (idx !== -1) { - const pluginInfo = ( - "plugin=" + decodeURIComponent(content.split("?plugin=")[1].split("&")[0]) - ).split(";"); - const params: Record = {}; - for (const item of pluginInfo) { - const [key, val] = item.split("="); - if (key) params[key] = val || true; // some options like "tls" will not have value + // plugin from `plugin=...` + const pluginParam = queryParams.plugin; + if (pluginParam) { + const pluginParts = pluginParam.split(";"); + const pluginName = pluginParts[0]; + const pluginOptions: Record = { plugin: pluginName }; + for (const raw of pluginParts.slice(1)) { + if (!raw) continue; + const [key, val] = splitOnce(raw, "="); + if (!key) continue; + pluginOptions[key] = val === undefined || val === "" ? true : val; } - switch (params.plugin) { + + switch (pluginOptions.plugin) { case "obfs-local": case "simple-obfs": proxy.plugin = "obfs"; proxy["plugin-opts"] = { - mode: params.obfs, - host: getIfNotBlank(params["obfs-host"]), + mode: pluginOptions.obfs, + host: getIfNotBlank(pluginOptions["obfs-host"]), }; break; case "v2ray-plugin": proxy.plugin = "v2ray-plugin"; proxy["plugin-opts"] = { mode: "websocket", - host: getIfNotBlank(params["obfs-host"]), - path: getIfNotBlank(params.path), - tls: getIfPresent(params.tls), + host: getIfNotBlank(pluginOptions["obfs-host"] ?? pluginOptions.host), + path: getIfNotBlank(pluginOptions.path), + tls: getIfPresent(pluginOptions.tls), }; break; default: - throw new Error(`Unsupported plugin option: ${params.plugin}`); + throw new Error(`Unsupported plugin option: ${pluginOptions.plugin}`); } } - if (/(&|\?)uot=(1|true)/i.test(query)) { + + // plugin from `v2ray-plugin=...` (base64 JSON) + const v2rayPluginParam = queryParams["v2ray-plugin"]; + if (!proxy.plugin && v2rayPluginParam) { + proxy.plugin = "v2ray-plugin"; + proxy["plugin-opts"] = JSON.parse(decodeBase64OrOriginal(v2rayPluginParam)); + } + + if ( + Object.prototype.hasOwnProperty.call(queryParams, "uot") && + parseBoolOrPresence(queryParams.uot) + ) { proxy["udp-over-tcp"] = true; } - if (/(&|\?)tfo=(1|true)/i.test(query)) { + if ( + Object.prototype.hasOwnProperty.call(queryParams, "tfo") && + parseBoolOrPresence(queryParams.tfo) + ) { proxy.tfo = true; } + return proxy; } function URI_SSR(line: string): IProxyshadowsocksRConfig { - line = decodeBase64OrOriginal(line.split("ssr://")[1]); + const afterScheme = stripUriScheme(line, "ssr", "Invalid ssr uri"); + if (!afterScheme) { + throw new Error("Invalid ssr uri"); + } + line = decodeBase64OrOriginal(afterScheme); // handle IPV6 & IPV4 format let splitIdx = line.indexOf(":origin"); if (splitIdx === -1) { splitIdx = line.indexOf(":auth_"); } + if (splitIdx === -1) { + throw new Error("Invalid ssr uri"); + } const serverAndPort = line.substring(0, splitIdx); - const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":")); - const port = parseInt( - serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1), + const portIdx = serverAndPort.lastIndexOf(":"); + if (portIdx === -1) { + throw new Error("Invalid ssr uri: missing port"); + } + const server = serverAndPort.substring(0, portIdx); + const port = parseRequiredPort( + serverAndPort.substring(portIdx + 1), + "Invalid ssr uri: invalid port", ); const params = line @@ -267,164 +593,97 @@ function URI_SSR(line: string): IProxyshadowsocksRConfig { }; // get other params - const other_params: Record = {}; - const paramsArray = line.split("/?")[1]?.split("&") || []; - for (const item of paramsArray) { - const [key, val] = item.split("="); - if (val?.trim().length > 0) { - other_params[key] = val.trim(); + const otherParams: Record = {}; + const rawOtherParams = parseQueryString(line.split("/?")[1]); + for (const [key, value] of Object.entries(rawOtherParams)) { + const trimmed = value?.trim(); + if (trimmed) { + otherParams[key] = trimmed; } } proxy = { ...proxy, - name: other_params.remarks - ? decodeBase64OrOriginal(other_params.remarks).trim() + name: otherParams.remarks + ? decodeBase64OrOriginal(otherParams.remarks).trim() : (proxy.server ?? ""), "protocol-param": getIfNotBlank( - decodeBase64OrOriginal(other_params.protoparam || "").replace(/\s/g, ""), + decodeBase64OrOriginal(otherParams.protoparam || "").replace(/\s/g, ""), ), "obfs-param": getIfNotBlank( - decodeBase64OrOriginal(other_params.obfsparam || "").replace(/\s/g, ""), + decodeBase64OrOriginal(otherParams.obfsparam || "").replace(/\s/g, ""), ), }; return proxy; } function URI_VMESS(line: string): IProxyVmessConfig { - line = line.split("vmess://")[1]; - let content = decodeBase64OrOriginal(line); + const afterScheme = stripUriScheme(line, "vmess", "Invalid vmess uri"); + if (!afterScheme) { + throw new Error("Invalid vmess uri"); + } + const raw = afterScheme; + const content = decodeBase64OrOriginal(raw); if (/=\s*vmess/.test(content)) { - // Quantumult VMess URI format - const partitions = content.split(",").map((p) => p.trim()); - const params: Record = {}; - for (const part of partitions) { - if (part.indexOf("=") !== -1) { - const [key, val] = part.split("="); - params[key.trim()] = val.trim(); - } - } + return parseVmessQuantumult(content); + } - const proxy: IProxyVmessConfig = { - name: partitions[0].split("=")[0].trim(), - type: "vmess", - server: partitions[1], - port: parseInt(partitions[2], 10), - cipher: getCipher(getIfNotBlank(partitions[3], "auto")), - uuid: partitions[4].match(/^"(.*)"$/)?.[1] || "", - tls: params.obfs === "wss", - udp: getIfPresent(params["udp-relay"]), - tfo: getIfPresent(params["fast-open"]), - "skip-cert-verify": isPresent(params["tls-verification"]) - ? !params["tls-verification"] - : undefined, - }; + const params = parseVmessParams(content, raw); + const server = params.add; + const port = parseRequiredPort( + params.port, + "Invalid vmess uri: invalid port", + ); + const tlsValue = params.tls; + const proxy: IProxyVmessConfig = { + name: + trimStr(params.ps) ?? + trimStr(params.remarks) ?? + trimStr(params.remark) ?? + `VMess ${server}:${port}`, + type: "vmess", + server, + port, + cipher: getCipher(getIfPresent(params.scy, "auto")), + uuid: params.id, + tls: + tlsValue === "tls" || + tlsValue === true || + tlsValue === 1 || + tlsValue === "1" || + tlsValue === "true", + "skip-cert-verify": isPresent(params.verify_cert) + ? !parseBool(params.verify_cert.toString()) + : undefined, + }; - if (isPresent(params.obfs)) { - if (params.obfs === "ws" || params.obfs === "wss") { - proxy.network = "ws"; - proxy["ws-opts"] = { - path: - (getIfNotBlank(params["obfs-path"]) || '"/"').match( - /^"(.*)"$/, - )?.[1] || "/", - headers: { - Host: - params["obfs-header"]?.match(/Host:\s*([a-zA-Z0-9-.]*)/)?.[1] || - "", - }, - }; - } else { - throw new Error(`Unsupported obfs: ${params.obfs}`); - } - } + proxy.alterId = parseInt(getIfPresent(params.aid ?? params.alterId, 0), 10); - return proxy; - } else { - let params: Record = {}; + if (proxy.tls && params.sni) { + proxy.servername = params.sni; + } - try { - // V2rayN URI format - params = JSON.parse(content); - } catch (e) { - // Shadowrocket URI format - console.warn( - "[URI_VMESS] JSON.parse(content) failed, falling back to Shadowrocket parsing:", - e, - ); - const match = /(^[^?]+?)\/?\?(.*)$/.exec(line); - if (match) { - const [_, base64Line, qs] = match; - content = decodeBase64OrOriginal(base64Line); + let httpupgrade = false; + if (params.net === "ws" || params.obfs === "websocket") { + proxy.network = "ws"; + } else if ( + ["http"].includes(params.net) || + ["http"].includes(params.obfs) || + ["http"].includes(params.type) + ) { + proxy.network = "http"; + } else if (["grpc"].includes(params.net)) { + proxy.network = "grpc"; + } else if (params.net === "httpupgrade") { + proxy.network = "ws"; + httpupgrade = true; + } else if (params.net === "h2" || proxy.network === "h2") { + proxy.network = "h2"; + } - for (const addon of qs.split("&")) { - const [key, valueRaw] = addon.split("="); - const value = decodeURIComponent(valueRaw); - if (value.indexOf(",") === -1) { - params[key] = value; - } else { - params[key] = value.split(","); - } - } - - const contentMatch = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content); - - if (contentMatch) { - const [__, cipher, uuid, server, port] = contentMatch; - - params.scy = cipher; - params.id = uuid; - params.port = port; - params.add = server; - } - } - } - - const server = params.add; - const port = parseInt(getIfPresent(params.port), 10); - const proxy: IProxyVmessConfig = { - name: - trimStr(params.ps) ?? - trimStr(params.remarks) ?? - trimStr(params.remark) ?? - `VMess ${server}:${port}`, - type: "vmess", - server, - port, - cipher: getCipher(getIfPresent(params.scy, "auto")), - uuid: params.id, - tls: ["tls", true, 1, "1"].includes(params.tls), - "skip-cert-verify": isPresent(params.verify_cert) - ? !params.verify_cert - : undefined, - }; - - proxy.alterId = parseInt(getIfPresent(params.aid ?? params.alterId, 0), 10); - - if (proxy.tls && params.sni) { - proxy.servername = params.sni; - } - - let httpupgrade = false; - if (params.net === "ws" || params.obfs === "websocket") { - proxy.network = "ws"; - } else if ( - ["http"].includes(params.net) || - ["http"].includes(params.obfs) || - ["http"].includes(params.type) - ) { - proxy.network = "http"; - } else if (["grpc"].includes(params.net)) { - proxy.network = "grpc"; - } else if (params.net === "httpupgrade") { - proxy.network = "ws"; - httpupgrade = true; - } else if (params.net === "h2" || proxy.network === "h2") { - proxy.network = "h2"; - } - - if (proxy.network) { - let transportHost = params.host ?? params.obfsParam; + if (proxy.network) { + let transportHost: any = params.host ?? params.obfsParam; + if (typeof transportHost === "string") { try { const parsedObfs = JSON.parse(transportHost); const parsedHost = parsedObfs?.Host; @@ -433,124 +692,186 @@ function URI_VMESS(line: string): IProxyVmessConfig { } } catch (e) { console.warn("[URI_VMESS] transportHost JSON.parse failed:", e); - // ignore JSON parse errors - } - - let transportPath = params.path; - if (proxy.network === "http") { - if (transportHost) { - transportHost = Array.isArray(transportHost) - ? transportHost[0] - : transportHost; - } - if (transportPath) { - transportPath = Array.isArray(transportPath) - ? transportPath[0] - : transportPath; - } else { - transportPath = "/"; - } - } - - if (transportPath || transportHost) { - if (["grpc"].includes(proxy.network)) { - proxy[`grpc-opts`] = { - "grpc-service-name": getIfNotBlank(transportPath), - }; - } else { - const opts: Record = { - path: getIfNotBlank(transportPath), - headers: { Host: getIfNotBlank(transportHost) }, - }; - if (httpupgrade) { - opts["v2ray-http-upgrade"] = true; - opts["v2ray-http-upgrade-fast-open"] = true; - } - switch (proxy.network) { - case "ws": - proxy["ws-opts"] = opts; - break; - case "http": - proxy["http-opts"] = opts; - break; - case "h2": - proxy["h2-opts"] = opts; - break; - default: - break; - } - } - } else { - delete proxy.network; - } - - if (proxy.tls && !proxy.servername && transportHost) { - proxy.servername = transportHost; } } - return proxy; + const transportPath: any = params.path; + const hostFirst = getIfNotBlank(firstString(transportHost)); + const pathFirst = getIfNotBlank(firstString(transportPath)); + + switch (proxy.network) { + case "grpc": { + if (!hostFirst && !pathFirst) { + delete proxy.network; + break; + } + const serviceName = getIfNotBlank(pathFirst); + if (serviceName) { + proxy["grpc-opts"] = { "grpc-service-name": serviceName }; + } + break; + } + case "h2": { + if (!hostFirst && !pathFirst) { + delete proxy.network; + break; + } + const h2Opts: H2Options = {}; + if (hostFirst) h2Opts.host = hostFirst; + if (pathFirst) h2Opts.path = pathFirst; + if (Object.keys(h2Opts).length > 0) { + proxy["h2-opts"] = h2Opts; + } + break; + } + case "http": { + const hosts = Array.isArray(transportHost) + ? transportHost + .map((h: any) => String(h).trim()) + .filter((h: string) => h) + : hostFirst + ? [hostFirst] + : undefined; + + let paths = Array.isArray(transportPath) + ? transportPath + .map((p: any) => String(p).trim()) + .filter((p: string) => p) + : pathFirst + ? [pathFirst] + : []; + + if (paths.length === 0) paths = ["/"]; + + const httpOpts: HttpOptions = { path: paths }; + if (hosts && hosts.length > 0) { + httpOpts.headers = { Host: hosts }; + } + proxy["http-opts"] = httpOpts; + break; + } + case "ws": { + if (!hostFirst && !pathFirst && !httpupgrade) { + delete proxy.network; + break; + } + const wsOpts: WsOptions = { + path: pathFirst, + headers: hostFirst ? { Host: hostFirst } : undefined, + }; + if (httpupgrade) { + wsOpts["v2ray-http-upgrade"] = true; + wsOpts["v2ray-http-upgrade-fast-open"] = true; + } + proxy["ws-opts"] = wsOpts; + break; + } + default: + break; + } + + if (proxy.tls && !proxy.servername && hostFirst) { + proxy.servername = hostFirst; + } } + + return proxy; } /** * VLess URL Decode. */ function URI_VLESS(line: string): IProxyVlessConfig { - line = line.split("vless://")[1]; - let isShadowrocket; - let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; - if (!parsed) { - const [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line)!; - line = `${atob(base64)}${other}`; - parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; - isShadowrocket = true; - } - const [, uuidRaw, server, portStr, , addons = "", nameRaw] = parsed; - let uuid = uuidRaw; - let name = nameRaw; - if (isShadowrocket) { - uuid = uuidRaw.replace(/^.*?:/g, ""); + const afterScheme = stripUriScheme(line, "vless", "Invalid vless uri"); + if (!afterScheme) { + throw new Error("Invalid vless uri"); } - const port = parseInt(portStr, 10); - uuid = decodeURIComponent(uuid); - name = decodeURIComponent(name); + let rest = afterScheme; + let isShadowrocket = false; + + const parseVlessRest = ( + input: string, + ): { + uuidRaw: string; + server: string; + port: number; + addons?: string; + nameRaw?: string; + } => { + const parsed = parseUrlLike(input, { + requireAuth: true, + errorMessage: "Invalid vless uri", + }); + if (!parsed.port) { + throw new Error("Invalid vless uri: missing port"); + } + const port = parseRequiredPort( + parsed.port, + "Invalid vless uri: invalid port", + ); + return { + uuidRaw: parsed.auth, + server: parsed.host, + port, + addons: parsed.query, + nameRaw: parsed.fragment, + }; + }; + + let parsed: ReturnType; + try { + parsed = parseVlessRest(rest); + } catch { + const shadowMatch = /^(.*?)(\?.*?$)/.exec(rest); + if (!shadowMatch) { + throw new Error("Invalid vless uri"); + } + const [, base64Part, other] = shadowMatch; + rest = `${decodeBase64OrOriginal(base64Part)}${other}`; + parsed = parseVlessRest(rest); + isShadowrocket = true; + } + + const { uuidRaw, server, port, addons = "", nameRaw } = parsed; + + let uuid = uuidRaw; + if (isShadowrocket) { + uuid = uuid.replace(/^.*?:/g, ""); + } + uuid = safeDecodeURIComponent(uuid) ?? uuid; + + const params = parseQueryStringNormalized(addons); + const name = + decodeAndTrim(nameRaw) ?? + trimStr(params.remarks) ?? + trimStr(params.remark) ?? + `VLESS ${server}:${port}`; const proxy: IProxyVlessConfig = { type: "vless", - name: "", + name, server, port, uuid, }; - const params: Record = {}; - for (const addon of addons.split("&")) { - const [key, valueRaw] = addon.split("="); - const value = decodeURIComponent(valueRaw); - params[key] = value; - } - - proxy.name = - trimStr(name) ?? - trimStr(params.remarks) ?? - trimStr(params.remark) ?? - `VLESS ${server}:${port}`; - proxy.tls = (params.security && params.security !== "none") || undefined; - if (isShadowrocket && /TRUE|1/i.test(params.tls)) { + if (isShadowrocket && parseBool(params.tls) === true) { proxy.tls = true; params.security = params.security ?? "reality"; } + proxy.servername = params.sni || params.peer; - proxy.flow = params.flow ? "xtls-rprx-vision" : undefined; + proxy.flow = parseVlessFlow(params.flow); proxy["client-fingerprint"] = params.fp as ClientFingerprint; proxy.alpn = params.alpn ? params.alpn.split(",") : undefined; - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.allowInsecure); + if (Object.prototype.hasOwnProperty.call(params, "allowInsecure")) { + proxy["skip-cert-verify"] = parseBoolOrPresence(params.allowInsecure); + } - if (["reality"].includes(params.security)) { + if (params.security === "reality") { const opts: IProxyVlessConfig["reality-opts"] = {}; if (params.pbk) { opts["public-key"] = params.pbk; @@ -564,74 +885,94 @@ function URI_VLESS(line: string): IProxyVlessConfig { } let httpupgrade = false; - proxy["ws-opts"] = { - headers: undefined, - path: undefined, - }; - - proxy["http-opts"] = { - headers: undefined, - path: undefined, - }; - proxy["grpc-opts"] = {}; + let network: NetworkType = "tcp"; if (params.headerType === "http") { - proxy.network = "http"; - } else if (params.type === "ws") { - proxy.network = "ws"; - httpupgrade = true; + network = "http"; } else { - proxy.network = ["tcp", "ws", "http", "grpc", "h2"].includes(params.type) - ? (params.type as NetworkType) - : "tcp"; - } - if (!proxy.network && isShadowrocket && params.obfs) { - switch (params.type) { - case "sw": - proxy.network = "ws"; - break; - case "http": - proxy.network = "http"; - break; - case "h2": - proxy.network = "h2"; - break; - case "grpc": - proxy.network = "grpc"; - break; - default: { - break; - } + let type = params.type; + if (type === "websocket") type = "ws"; + if (isShadowrocket && type === "sw") type = "ws"; + if (type === "httpupgrade") { + network = "ws"; + httpupgrade = true; + } else if (type && ["tcp", "ws", "http", "grpc", "h2"].includes(type)) { + network = type as NetworkType; + } else { + network = "tcp"; + } + + if (params.type === "ws") { + httpupgrade = true; } } - if (["websocket"].includes(proxy.network)) { - proxy.network = "ws"; - } + + proxy.network = network; + if (proxy.network && !["tcp", "none"].includes(proxy.network)) { - const opts: Record = {}; const host = params.host ?? params.obfsParam; - if (host) { - if (params.obfsParam) { - try { - const parsed = JSON.parse(host); - opts.headers = parsed; - } catch (e) { - console.warn("[URI_VLESS] host JSON.parse failed:", e); - opts.headers = { Host: host }; + const path = params.path; + + switch (proxy.network) { + case "grpc": + { + const serviceName = getIfNotBlank(path); + if (serviceName) { + proxy["grpc-opts"] = { "grpc-service-name": serviceName }; + } } - } else { - opts.headers = { Host: host }; + break; + case "h2": { + const h2Opts: H2Options = {}; + const hostVal = getIfNotBlank(host); + const pathVal = getIfNotBlank(path); + if (hostVal) h2Opts.host = hostVal; + if (pathVal) h2Opts.path = pathVal; + if (Object.keys(h2Opts).length > 0) { + proxy["h2-opts"] = h2Opts; + } + break; } - } - if (params.path) { - opts.path = params.path; - } - if (httpupgrade) { - opts["v2ray-http-upgrade"] = true; - opts["v2ray-http-upgrade-fast-open"] = true; - } - if (Object.keys(opts).length > 0) { - proxy[`ws-opts`] = opts; + case "http": { + const httpOpts: HttpOptions = {}; + const hostVal = getIfNotBlank(host); + const pathVal = getIfNotBlank(path); + if (pathVal) httpOpts.path = [pathVal]; + if (hostVal) httpOpts.headers = { Host: [hostVal] }; + if (Object.keys(httpOpts).length > 0) { + proxy["http-opts"] = httpOpts; + } + break; + } + case "ws": { + const wsOpts: WsOptions = {}; + if (host) { + if (params.obfsParam) { + try { + const parsedHeaders = JSON.parse(host); + wsOpts.headers = parsedHeaders; + } catch (e) { + console.warn("[URI_VLESS] host JSON.parse failed:", e); + wsOpts.headers = { Host: host }; + } + } else { + wsOpts.headers = { Host: host }; + } + } + if (path) { + wsOpts.path = path; + } + if (httpupgrade) { + wsOpts["v2ray-http-upgrade"] = true; + wsOpts["v2ray-http-upgrade-fast-open"] = true; + } + if (Object.keys(wsOpts).length > 0) { + proxy["ws-opts"] = wsOpts; + } + break; + } + default: + break; } } @@ -639,30 +980,33 @@ function URI_VLESS(line: string): IProxyVlessConfig { if (proxy.network === "ws") { proxy.servername = proxy["ws-opts"]?.headers?.Host; } else if (proxy.network === "http") { - const httpHost = proxy["http-opts"]?.headers?.Host; - proxy.servername = Array.isArray(httpHost) ? httpHost[0] : httpHost; + proxy.servername = proxy["http-opts"]?.headers?.Host?.[0]; + } else if (proxy.network === "h2") { + proxy.servername = proxy["h2-opts"]?.host; } } + return proxy; } function URI_Trojan(line: string): IProxyTrojanConfig { - line = line.split("trojan://")[1]; - const [, passwordRaw, server, , port, , addons = "", nameRaw] = - /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; - - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme(line, "trojan", "Invalid trojan uri"); + if (!afterScheme) { + throw new Error("Invalid trojan uri"); } - - let password = passwordRaw; - password = decodeURIComponent(password); - - let name = nameRaw; - const decodedName = trimStr(decodeURIComponent(name)); - - name = decodedName ?? `Trojan ${server}:${portNum}`; + const { + auth: passwordRaw, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { + requireAuth: true, + errorMessage: "Invalid trojan uri", + }); + const portNum = parsePortOrDefault(port, 443); + const password = safeDecodeURIComponent(passwordRaw) ?? passwordRaw; + const name = decodeAndTrim(nameRaw) ?? `Trojan ${server}:${portNum}`; const proxy: IProxyTrojanConfig = { type: "trojan", name, @@ -670,88 +1014,88 @@ function URI_Trojan(line: string): IProxyTrojanConfig { port: portNum, password, }; - let host = ""; - let path = ""; - for (const addon of addons.split("&")) { - const [key, valueRaw] = addon.split("="); - const value = decodeURIComponent(valueRaw); - switch (key) { - case "type": - if (["ws", "h2"].includes(value)) { - proxy.network = value as NetworkType; - } else { - proxy.network = "tcp"; - } - break; - case "host": - host = value; - break; - case "path": - path = value; - break; - case "alpn": - proxy["alpn"] = value ? value.split(",") : undefined; - break; - case "sni": - proxy["sni"] = value; - break; - case "skip-cert-verify": - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value); - break; - case "fingerprint": - proxy["fingerprint"] = value; - break; - case "fp": - proxy["fingerprint"] = value; - break; - case "encryption": - { - const encryption = value.split(";"); - if (encryption.length === 3) { - proxy["ss-opts"] = { - enabled: true, - method: encryption[1], - password: encryption[2], - }; - } - } - break; - case "client-fingerprint": - proxy["client-fingerprint"] = value as ClientFingerprint; - break; - default: - break; + const params = parseQueryStringNormalized(addons); + + const network = params.type; + if (network && ["ws", "grpc", "h2", "tcp"].includes(network)) { + proxy.network = network as NetworkType; + } + + const host = getIfNotBlank(params.host); + const path = getIfNotBlank(params.path); + + if (params.alpn) { + proxy.alpn = params.alpn.split(","); + } + if (params.sni) { + proxy.sni = params.sni; + } + if (Object.prototype.hasOwnProperty.call(params, "skip-cert-verify")) { + proxy["skip-cert-verify"] = parseBoolOrPresence(params["skip-cert-verify"]); + } + + proxy.fingerprint = params.fingerprint ?? params.fp; + + if (params.encryption) { + const encryption = params.encryption.split(";"); + if (encryption.length === 3) { + proxy["ss-opts"] = { + enabled: true, + method: encryption[1], + password: encryption[2], + }; } } + + if (params["client-fingerprint"]) { + proxy["client-fingerprint"] = params[ + "client-fingerprint" + ] as ClientFingerprint; + } + if (proxy.network === "ws") { - proxy["ws-opts"] = { - headers: { Host: host }, - path, - } as WsOptions; + const wsOpts: WsOptions = {}; + if (host) wsOpts.headers = { Host: host }; + if (path) wsOpts.path = path; + if (Object.keys(wsOpts).length > 0) { + proxy["ws-opts"] = wsOpts; + } } else if (proxy.network === "grpc") { - proxy["grpc-opts"] = { - "grpc-service-name": path, - } as GrpcOptions; + const serviceName = getIfNotBlank(path); + if (serviceName) { + proxy["grpc-opts"] = { "grpc-service-name": serviceName }; + } } return proxy; } function URI_Hysteria2(line: string): IProxyHysteria2Config { - line = line.split(/(hysteria2|hy2):\/\//)[2]; - - const [, passwordRaw, server, , port, , addons = "", nameRaw] = - /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme( + line, + ["hysteria2", "hy2"], + "Invalid hysteria2 uri", + ); + if (!afterScheme) { + throw new Error("Invalid hysteria2 uri"); } - const password = decodeURIComponent(passwordRaw); + const { + auth: passwordRaw, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { + requireAuth: true, + errorMessage: "Invalid hysteria2 uri", + }); + const portNum = parsePortOrDefault(port, 443); + const password = safeDecodeURIComponent(passwordRaw) ?? passwordRaw; - const decodedName = trimStr(decodeURIComponent(nameRaw)); + const decodedName = decodeAndTrim(nameRaw); - const name = decodedName ?? `Hysteria2 ${server}:${port}`; + const name = decodedName ?? `Hysteria2 ${server}:${portNum}`; const proxy: IProxyHysteria2Config = { type: "hysteria2", @@ -761,13 +1105,7 @@ function URI_Hysteria2(line: string): IProxyHysteria2Config { password, }; - const params: Record = {}; - for (const addon of addons.split("&")) { - const [key, valueRaw] = addon.split("="); - let value = valueRaw; - value = decodeURIComponent(valueRaw); - params[key] = value; - } + const params = parseQueryStringNormalized(addons); proxy.sni = params.sni; if (!proxy.sni && params.peer) { @@ -779,24 +1117,34 @@ function URI_Hysteria2(line: string): IProxyHysteria2Config { proxy.ports = params.mport; proxy["obfs-password"] = params["obfs-password"]; - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.insecure); - proxy.tfo = /(TRUE)|1/i.test(params.fastopen); + if (Object.prototype.hasOwnProperty.call(params, "insecure")) { + proxy["skip-cert-verify"] = parseBoolOrPresence(params.insecure); + } + if (Object.prototype.hasOwnProperty.call(params, "fastopen")) { + proxy.tfo = parseBoolOrPresence(params.fastopen); + } proxy.fingerprint = params.pinSHA256; return proxy; } function URI_Hysteria(line: string): IProxyHysteriaConfig { - line = line.split(/(hysteria|hy):\/\//)[2]; - const [, server, , port, , addons = "", nameRaw] = - /^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme( + line, + ["hysteria", "hy"], + "Invalid hysteria uri", + ); + if (!afterScheme) { + throw new Error("Invalid hysteria uri"); } - const decodedName = trimStr(decodeURIComponent(nameRaw)); - - const name = decodedName ?? `Hysteria ${server}:${port}`; + const { + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { errorMessage: "Invalid hysteria uri" }); + const portNum = parsePortOrDefault(port, 443); + const name = decodeAndTrim(nameRaw) ?? `Hysteria ${server}:${portNum}`; const proxy: IProxyHysteriaConfig = { type: "hysteria", @@ -804,78 +1152,70 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig { server, port: portNum, }; - const params: Record = {}; - for (const addon of addons.split("&")) { - let [key, value] = addon.split("="); - key = key.replace(/_/, "-"); - value = decodeURIComponent(value); + const params = parseQueryStringNormalized(addons); + + for (const [key, value] of Object.entries(params)) { switch (key) { case "alpn": - proxy["alpn"] = value ? value.split(",") : undefined; + proxy.alpn = value ? value.split(",") : undefined; break; case "insecure": - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value); + proxy["skip-cert-verify"] = parseBoolOrPresence(value); break; case "auth": - proxy["auth-str"] = value; + if (value) proxy["auth-str"] = value; break; case "mport": - proxy["ports"] = value; + if (value) proxy.ports = value; break; case "obfsParam": - proxy["obfs"] = value; + if (value) proxy.obfs = value; break; case "upmbps": - proxy["up"] = value; + if (value) proxy.up = value; break; case "downmbps": - proxy["down"] = value; + if (value) proxy.down = value; break; case "obfs": - proxy["obfs"] = value || ""; + if (value !== undefined) proxy.obfs = value || ""; break; case "fast-open": - proxy["fast-open"] = /(TRUE)|1/i.test(value); + proxy["fast-open"] = parseBoolOrPresence(value); break; case "peer": - proxy["fast-open"] = /(TRUE)|1/i.test(value); + if (!proxy.sni && value) proxy.sni = value; break; case "recv-window-conn": - proxy["recv-window-conn"] = parseInt(value); + proxy["recv-window-conn"] = parseInteger(value); break; case "recv-window": - proxy["recv-window"] = parseInt(value); + proxy["recv-window"] = parseInteger(value); break; case "ca": - proxy["ca"] = value; + if (value) proxy.ca = value; break; case "ca-str": - proxy["ca-str"] = value; + if (value) proxy["ca-str"] = value; break; case "disable-mtu-discovery": - proxy["disable-mtu-discovery"] = /(TRUE)|1/i.test(value); + proxy["disable-mtu-discovery"] = parseBoolOrPresence(value); break; case "fingerprint": - proxy["fingerprint"] = value; + if (value) proxy.fingerprint = value; break; case "protocol": - proxy["protocol"] = value; + if (value) proxy.protocol = value; break; case "sni": - proxy["sni"] = value; + if (value) proxy.sni = value; break; default: break; } } - if (!proxy.sni && params.peer) { - proxy.sni = params.peer; - } - if (!proxy["fast-open"] && params["fast-open"]) { - proxy["fast-open"] = true; - } if (!proxy.protocol) { proxy.protocol = "udp"; } @@ -884,19 +1224,30 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig { } function URI_TUIC(line: string): IProxyTuicConfig { - line = line.split(/tuic:\/\//)[1]; - - const [, uuid, passwordRaw, server, , port, , addons = "", nameRaw] = - /^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; - - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme(line, "tuic", "Invalid tuic uri"); + if (!afterScheme) { + throw new Error("Invalid tuic uri"); + } + const { + auth, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { + requireAuth: true, + errorMessage: "Invalid tuic uri", + }); + const [uuid, passwordRaw] = splitOnce(auth, ":"); + if (passwordRaw === undefined) { + throw new Error("Invalid tuic uri"); } - const password = decodeURIComponent(passwordRaw); - const decodedName = trimStr(decodeURIComponent(nameRaw)); - const name = decodedName ?? `TUIC ${server}:${port}`; + const portNum = parsePortOrDefault(port, 443); + const password = safeDecodeURIComponent(passwordRaw) ?? passwordRaw; + const decodedName = decodeAndTrim(nameRaw); + + const name = decodedName ?? `TUIC ${server}:${portNum}`; const proxy: IProxyTuicConfig = { type: "tuic", @@ -907,31 +1258,29 @@ function URI_TUIC(line: string): IProxyTuicConfig { uuid, }; - for (const addon of addons.split("&")) { - let [key, value] = addon.split("="); - key = key.replace(/_/, "-"); - value = decodeURIComponent(value); + const params = parseQueryStringNormalized(addons); + for (const [key, value] of Object.entries(params)) { switch (key) { case "token": - proxy["token"] = value; + proxy.token = value; break; case "ip": - proxy["ip"] = value; + proxy.ip = value; break; case "heartbeat-interval": - proxy["heartbeat-interval"] = parseInt(value); + proxy["heartbeat-interval"] = parseInteger(value); break; case "alpn": - proxy["alpn"] = value ? value.split(",") : undefined; + proxy.alpn = value ? value.split(",") : undefined; break; case "disable-sni": - proxy["disable-sni"] = /(TRUE)|1/i.test(value); + proxy["disable-sni"] = parseBoolOrPresence(value); break; case "reduce-rtt": - proxy["reduce-rtt"] = /(TRUE)|1/i.test(value); + proxy["reduce-rtt"] = parseBoolOrPresence(value); break; case "request-timeout": - proxy["request-timeout"] = parseInt(value); + proxy["request-timeout"] = parseInteger(value); break; case "udp-relay-mode": proxy["udp-relay-mode"] = value; @@ -940,22 +1289,22 @@ function URI_TUIC(line: string): IProxyTuicConfig { proxy["congestion-controller"] = value; break; case "max-udp-relay-packet-size": - proxy["max-udp-relay-packet-size"] = parseInt(value); + proxy["max-udp-relay-packet-size"] = parseInteger(value); break; case "fast-open": - proxy["fast-open"] = /(TRUE)|1/i.test(value); + proxy["fast-open"] = parseBoolOrPresence(value); break; case "skip-cert-verify": - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value); + case "allow-insecure": + proxy["skip-cert-verify"] = parseBoolOrPresence(value); break; case "max-open-streams": - proxy["max-open-streams"] = parseInt(value); + proxy["max-open-streams"] = parseInteger(value); break; case "sni": - proxy["sni"] = value; + proxy.sni = value; break; - case "allow-insecure": - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value); + default: break; } } @@ -964,18 +1313,26 @@ function URI_TUIC(line: string): IProxyTuicConfig { } function URI_Wireguard(line: string): IProxyWireguardConfig { - line = line.split(/(wireguard|wg):\/\//)[2]; - const [, , privateKeyRaw, server, , port, , addons = "", nameRaw] = - /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; - - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme( + line, + ["wireguard", "wg"], + "Invalid wireguard uri", + ); + if (!afterScheme) { + throw new Error("Invalid wireguard uri"); } - const privateKey = decodeURIComponent(privateKeyRaw); - const decodedName = trimStr(decodeURIComponent(nameRaw)); + const { + auth: privateKeyRaw, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { errorMessage: "Invalid wireguard uri" }); + const portNum = parsePortOrDefault(port, 443); + const privateKey = safeDecodeURIComponent(privateKeyRaw) ?? privateKeyRaw; + const decodedName = decodeAndTrim(nameRaw); - const name = decodedName ?? `WireGuard ${server}:${port}`; + const name = decodedName ?? `WireGuard ${server}:${portNum}`; const proxy: IProxyWireguardConfig = { type: "wireguard", name, @@ -984,14 +1341,14 @@ function URI_Wireguard(line: string): IProxyWireguardConfig { "private-key": privateKey, udp: true, }; - for (const addon of addons.split("&")) { - let [key, value] = addon.split("="); - key = key.replace(/_/, "-"); - value = decodeURIComponent(value); + + const params = parseQueryStringNormalized(addons); + for (const [key, value] of Object.entries(params)) { switch (key) { case "address": case "ip": - value.split(",").map((i) => { + if (!value) break; + value.split(",").forEach((i) => { const ip = i .trim() .replace(/\/\d+$/, "") @@ -1005,38 +1362,44 @@ function URI_Wireguard(line: string): IProxyWireguardConfig { }); break; case "publickey": + case "public-key": + if (!value) break; proxy["public-key"] = value; break; case "allowed-ips": + if (!value) break; proxy["allowed-ips"] = value.split(","); break; case "pre-shared-key": + if (!value) break; proxy["pre-shared-key"] = value; break; case "reserved": { + if (!value) break; const parsed = value .split(",") - .map((i) => parseInt(i.trim(), 10)) - .filter((i) => Number.isInteger(i)); + .map((i) => parseInteger(i.trim())) + .filter((i): i is number => Number.isInteger(i)); if (parsed.length === 3) { proxy["reserved"] = parsed; } } break; case "udp": - proxy["udp"] = /(TRUE)|1/i.test(value); + proxy.udp = parseBoolOrPresence(value); break; case "mtu": - proxy.mtu = parseInt(value.trim(), 10); + proxy.mtu = parseInteger(value?.trim()); break; case "dialer-proxy": proxy["dialer-proxy"] = value; break; case "remote-dns-resolve": - proxy["remote-dns-resolve"] = /(TRUE)|1/i.test(value); + proxy["remote-dns-resolve"] = parseBoolOrPresence(value); break; case "dns": + if (!value) break; proxy.dns = value.split(","); break; default: @@ -1048,20 +1411,24 @@ function URI_Wireguard(line: string): IProxyWireguardConfig { } function URI_HTTP(line: string): IProxyHttpConfig { - line = line.split(/(http|https):\/\//)[2]; - const [, , authRaw, server, , port, , addons = "", nameRaw] = - /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; - - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme( + line, + ["http", "https"], + "Invalid http uri", + ); + if (!afterScheme) { + throw new Error("Invalid http uri"); } - let auth = authRaw; - - if (auth) { - auth = decodeURIComponent(auth); - } - const decodedName = trimStr(decodeURIComponent(nameRaw)); + const { + auth: authRaw, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { errorMessage: "Invalid http uri" }); + const portNum = parsePortOrDefault(port, 443); + const auth = safeDecodeURIComponent(authRaw) ?? authRaw; + const decodedName = decodeAndTrim(nameRaw); const name = decodedName ?? `HTTP ${server}:${portNum}`; const proxy: IProxyHttpConfig = { @@ -1071,39 +1438,25 @@ function URI_HTTP(line: string): IProxyHttpConfig { port: portNum, }; if (auth) { - const [username, password] = auth.split(":"); + const [username, password] = splitOnce(auth, ":"); proxy.username = username; proxy.password = password; } - for (const addon of addons.split("&")) { - let [key, value] = addon.split("="); - key = key.replace(/_/, "-"); - value = decodeURIComponent(value); + const params = parseQueryStringNormalized(addons); + for (const [key, value] of Object.entries(params)) { switch (key) { case "tls": - proxy.tls = /(TRUE)|1/i.test(value); + proxy.tls = parseBoolOrPresence(value); break; case "fingerprint": - proxy["fingerprint"] = value; + proxy.fingerprint = value; break; case "skip-cert-verify": - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value); + proxy["skip-cert-verify"] = parseBoolOrPresence(value); break; case "ip-version": - if ( - ["dual", "ipv4", "ipv6", "ipv4-prefer", "ipv6-prefer"].includes(value) - ) { - proxy["ip-version"] = value as - | "dual" - | "ipv4" - | "ipv6" - | "ipv4-prefer" - | "ipv6-prefer"; - } else { - proxy["ip-version"] = "dual"; - } - + proxy["ip-version"] = parseIpVersion(value); break; default: break; @@ -1114,20 +1467,25 @@ function URI_HTTP(line: string): IProxyHttpConfig { } function URI_SOCKS(line: string): IProxySocks5Config { - line = line.split(/socks5:\/\//)[1]; - const [, , authRaw, server, , port, , addons = "", nameRaw] = - /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; - - let portNum = parseInt(`${port}`, 10); - if (isNaN(portNum)) { - portNum = 443; + const afterScheme = stripUriScheme( + line, + ["socks5", "socks"], + "Invalid socks uri", + ); + if (!afterScheme) { + throw new Error("Invalid socks uri"); } + const { + auth: authRaw, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { errorMessage: "Invalid socks uri" }); + const portNum = parsePortOrDefault(port, 443); - let auth = authRaw; - if (auth) { - auth = decodeURIComponent(auth); - } - const decodedName = trimStr(decodeURIComponent(nameRaw)); + const auth = safeDecodeURIComponent(authRaw) ?? authRaw; + const decodedName = decodeAndTrim(nameRaw); const name = decodedName ?? `SOCKS5 ${server}:${portNum}`; const proxy: IProxySocks5Config = { type: "socks5", @@ -1136,41 +1494,28 @@ function URI_SOCKS(line: string): IProxySocks5Config { port: portNum, }; if (auth) { - const [username, password] = auth.split(":"); + const [username, password] = splitOnce(auth, ":"); proxy.username = username; proxy.password = password; } - for (const addon of addons.split("&")) { - let [key, value] = addon.split("="); - key = key.replace(/_/, "-"); - value = decodeURIComponent(value); + const params = parseQueryStringNormalized(addons); + for (const [key, value] of Object.entries(params)) { switch (key) { case "tls": - proxy.tls = /(TRUE)|1/i.test(value); + proxy.tls = parseBoolOrPresence(value); break; case "fingerprint": - proxy["fingerprint"] = value; + proxy.fingerprint = value; break; case "skip-cert-verify": - proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value); + proxy["skip-cert-verify"] = parseBoolOrPresence(value); break; case "udp": - proxy["udp"] = /(TRUE)|1/i.test(value); + proxy.udp = parseBoolOrPresence(value); break; case "ip-version": - if ( - ["dual", "ipv4", "ipv6", "ipv4-prefer", "ipv6-prefer"].includes(value) - ) { - proxy["ip-version"] = value as - | "dual" - | "ipv4" - | "ipv6" - | "ipv4-prefer" - | "ipv6-prefer"; - } else { - proxy["ip-version"] = "dual"; - } + proxy["ip-version"] = parseIpVersion(value); break; default: break; diff --git a/clash-verge-rev/vite.config.mts b/clash-verge-rev/vite.config.mts index bc47a4c440..a5ef33af2c 100644 --- a/clash-verge-rev/vite.config.mts +++ b/clash-verge-rev/vite.config.mts @@ -1,10 +1,14 @@ import path from "node:path"; +import { fileURLToPath } from "node:url"; import legacy from "@vitejs/plugin-legacy"; import react from "@vitejs/plugin-react-swc"; import { defineConfig } from "vite"; import svgr from "vite-plugin-svgr"; +const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url)); +const SRC_ROOT = path.resolve(CONFIG_DIR, "src"); + const getPackageName = (id: string) => { // Walk through possible pnpm virtual paths and nested node_modules, return the last package segment. const matches = [ @@ -16,6 +20,57 @@ const getPackageName = (id: string) => { return last ? last[1] : null; }; +const getSemanticNameFromFacade = ( + facadeModuleId: string | null | undefined, +) => { + if (!facadeModuleId) return null; + + const cleanPath = facadeModuleId.split("?")[0]; + const relativePath = path.relative(SRC_ROOT, cleanPath); + if (relativePath.startsWith("..")) return null; + + const withoutExtension = relativePath.replace(/\.[^/.]+$/, ""); + const normalizedPath = withoutExtension.replace(/\\/g, "/"); + const withoutTrailingIndex = normalizedPath.endsWith("/index") + ? normalizedPath.slice(0, -"/index".length) + : normalizedPath; + + const semanticName = withoutTrailingIndex + .split("/") + .filter(Boolean) + .join("-") + .replace(/[^a-zA-Z0-9-]/g, "-") + .replace(/--+/g, "-") + .replace(/^-+|-+$/g, ""); + + return semanticName || "index"; +}; + +const normalizeEntryName = (name: string, isEntry?: boolean) => + isEntry && name === "index" ? "main" : name; + +const getSemanticNameFromChunk = (chunkInfo: { + facadeModuleId: string | null | undefined; + moduleIds?: string[]; + name: string; + isEntry?: boolean; +}) => { + const fromFacade = getSemanticNameFromFacade(chunkInfo.facadeModuleId); + if (fromFacade) return normalizeEntryName(fromFacade, chunkInfo.isEntry); + + if (Array.isArray(chunkInfo.moduleIds)) { + for (const moduleId of chunkInfo.moduleIds) { + const semantic = getSemanticNameFromFacade(moduleId); + if (semantic) return normalizeEntryName(semantic, chunkInfo.isEntry); + } + } + + return normalizeEntryName(chunkInfo.name, chunkInfo.isEntry); +}; + +const normalizePackageName = (pkg: string) => + pkg.replace(/^@/, "").replace(/[@/]/g, "-"); + type ChunkMatchContext = { id: string; pkg: string | null }; type ChunkRule = { name: string | ((pkg: string) => string); @@ -87,7 +142,7 @@ const chunkRules: ChunkRule[] = [ ...packageSetRules, ...namespaceRules, { - name: (pkg) => `vendor-${pkg.replace(/[@/]/g, "-")}`, + name: (pkg) => `vendor-${normalizePackageName(pkg ?? "vendor")}`, match: ({ pkg }) => !!pkg && LARGE_VENDOR_MATCHERS.some((keyword) => pkg.includes(keyword)), }, @@ -141,6 +196,16 @@ export default defineConfig({ output: { compact: true, dynamicImportInCjs: true, + entryFileNames: (chunkInfo) => { + const semanticName = getSemanticNameFromChunk(chunkInfo); + + return `assets/${semanticName}-[hash].js`; + }, + chunkFileNames: (chunkInfo) => { + const semanticName = getSemanticNameFromChunk(chunkInfo); + + return `assets/${semanticName}-[hash].js`; + }, manualChunks(id) { if (!id.includes("node_modules")) return; diff --git a/lede/package/kernel/linux/modules/gpio.mk b/lede/package/kernel/linux/modules/gpio.mk index fc6ab66ba8..2a118eda29 100644 --- a/lede/package/kernel/linux/modules/gpio.mk +++ b/lede/package/kernel/linux/modules/gpio.mk @@ -137,3 +137,20 @@ define KernelPackage/gpio-pcf857x/description endef $(eval $(call KernelPackage,gpio-pcf857x)) + + +define KernelPackage/gpio-pwm + SUBMENU:=$(GPIO_MENU) + DEPENDS:=@GPIO_SUPPORT @PWM_SUPPORT @LINUX_6_12 + TITLE:=PWM GPIO support + KCONFIG:=CONFIG_PWM_GPIO + FILES:=$(LINUX_DIR)/drivers/pwm/pwm-gpio.ko + AUTOLOAD:=$(call AutoProbe,pwm-gpio) +endef + +define KernelPackage/gpio-pwm/description + Generic PWM framework driver for software PWM toggling a GPIO pin from + kernel high-resolution timers. +endef + +$(eval $(call KernelPackage,gpio-pwm)) diff --git a/mihomo/component/ca/config.go b/mihomo/component/ca/config.go index 9cc8839f40..f50780af9f 100644 --- a/mihomo/component/ca/config.go +++ b/mihomo/component/ca/config.go @@ -10,7 +10,6 @@ import ( "sync" "github.com/metacubex/mihomo/common/once" - C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/tls" @@ -107,12 +106,13 @@ func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) { } if len(opt.Certificate) > 0 || len(opt.PrivateKey) > 0 { - var cert tls.Certificate - cert, err = LoadTLSKeyPair(opt.Certificate, opt.PrivateKey, C.Path) + certLoader, err := NewTLSKeyPairLoader(opt.Certificate, opt.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return certLoader() + } } return tlsConfig, nil } diff --git a/mihomo/component/ca/keypair.go b/mihomo/component/ca/keypair.go index 13b38dc11f..ff9086d1b7 100644 --- a/mihomo/component/ca/keypair.go +++ b/mihomo/component/ca/keypair.go @@ -12,67 +12,80 @@ import ( "fmt" "math/big" "os" + "runtime" + "sync" "time" + C "github.com/metacubex/mihomo/constant" + + "github.com/metacubex/fswatch" "github.com/metacubex/tls" ) -type Path interface { - Resolve(path string) string - IsSafePath(path string) bool - ErrNotSafePath(path string) error -} - -// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution. -// Returns a tls.Certificate and an error, where the error indicates issues during parsing or file loading. +// NewTLSKeyPairLoader creates a loader function for TLS key pairs from the provided certificate and private key data or file paths. // If both certificate and privateKey are empty, generates a random TLS RSA key pair. -// Accepts a Path interface for resolving file paths when necessary. -func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, error) { +func NewTLSKeyPairLoader(certificate, privateKey string) (func() (*tls.Certificate, error), error) { if certificate == "" && privateKey == "" { var err error certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA) if err != nil { - return tls.Certificate{}, err + return nil, err } } cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) if painTextErr == nil { - return cert, nil - } - if path == nil { - return tls.Certificate{}, painTextErr + return func() (*tls.Certificate, error) { + return &cert, nil + }, nil } - certificate = path.Resolve(certificate) - privateKey = path.Resolve(privateKey) + certificate = C.Path.Resolve(certificate) + privateKey = C.Path.Resolve(privateKey) var loadErr error - if !path.IsSafePath(certificate) { - loadErr = path.ErrNotSafePath(certificate) - } else if !path.IsSafePath(privateKey) { - loadErr = path.ErrNotSafePath(privateKey) + if !C.Path.IsSafePath(certificate) { + loadErr = C.Path.ErrNotSafePath(certificate) + } else if !C.Path.IsSafePath(privateKey) { + loadErr = C.Path.ErrNotSafePath(privateKey) } else { cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) } if loadErr != nil { - return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } - return cert, nil + gcFlag := new(os.File) + updateMutex := sync.RWMutex{} + if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) { + updateMutex.Lock() + defer updateMutex.Unlock() + if newCert, err := tls.LoadX509KeyPair(certificate, privateKey); err == nil { + cert = newCert + } + }}); err == nil { + if err = watcher.Start(); err == nil { + runtime.SetFinalizer(gcFlag, func(f *os.File) { + _ = watcher.Close() + }) + } + } + return func() (*tls.Certificate, error) { + defer runtime.KeepAlive(gcFlag) + updateMutex.RLock() + defer updateMutex.RUnlock() + return &cert, nil + }, nil } -func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) { +func LoadCertificates(certificate string) (*x509.CertPool, error) { pool := x509.NewCertPool() if pool.AppendCertsFromPEM([]byte(certificate)) { return pool, nil } painTextErr := fmt.Errorf("invalid certificate: %s", certificate) - if path == nil { - return nil, painTextErr - } - certificate = path.Resolve(certificate) + certificate = C.Path.Resolve(certificate) var loadErr error - if !path.IsSafePath(certificate) { - loadErr = path.ErrNotSafePath(certificate) + if !C.Path.IsSafePath(certificate) { + loadErr = C.Path.ErrNotSafePath(certificate) } else { certPEMBlock, err := os.ReadFile(certificate) if pool.AppendCertsFromPEM(certPEMBlock) { @@ -83,6 +96,9 @@ func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) { if loadErr != nil { return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } + //TODO: support dynamic update pool too + // blocked by: https://github.com/golang/go/issues/64796 + // maybe we can direct add `GetRootCAs` and `GetClientCAs` to ourselves tls fork return pool, nil } diff --git a/mihomo/component/ech/key.go b/mihomo/component/ech/key.go index b0d572d628..4911b90f4c 100644 --- a/mihomo/component/ech/key.go +++ b/mihomo/component/ech/key.go @@ -8,9 +8,12 @@ import ( "errors" "fmt" "os" + "runtime" + "sync" - "github.com/metacubex/mihomo/component/ca" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/fswatch" "github.com/metacubex/tls" "golang.org/x/crypto/cryptobyte" ) @@ -104,40 +107,65 @@ func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { return keys, nil } -func LoadECHKey(key string, tlsConfig *tls.Config, path ca.Path) error { +func LoadECHKey(key string, tlsConfig *tls.Config) error { if key == "" { return nil } - painTextErr := loadECHKey([]byte(key), tlsConfig) + echKeys, painTextErr := loadECHKey([]byte(key)) if painTextErr == nil { + tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + return echKeys, nil + } return nil } - key = path.Resolve(key) + key = C.Path.Resolve(key) var loadErr error - if !path.IsSafePath(key) { - loadErr = path.ErrNotSafePath(key) + if !C.Path.IsSafePath(key) { + loadErr = C.Path.ErrNotSafePath(key) } else { var echKey []byte echKey, loadErr = os.ReadFile(key) if loadErr == nil { - loadErr = loadECHKey(echKey, tlsConfig) + echKeys, loadErr = loadECHKey(echKey) } } if loadErr != nil { return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } + gcFlag := new(os.File) + updateMutex := sync.RWMutex{} + if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) { + updateMutex.Lock() + defer updateMutex.Unlock() + if echKey, err := os.ReadFile(key); err == nil { + if newEchKeys, err := loadECHKey(echKey); err == nil { + echKeys = newEchKeys + } + } + }}); err == nil { + if err = watcher.Start(); err == nil { + runtime.SetFinalizer(gcFlag, func(f *os.File) { + _ = watcher.Close() + }) + } + } + tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { + defer runtime.KeepAlive(gcFlag) + updateMutex.RLock() + defer updateMutex.RUnlock() + return echKeys, nil + } return nil } -func loadECHKey(echKey []byte, tlsConfig *tls.Config) error { +func loadECHKey(echKey []byte) ([]tls.EncryptedClientHelloKey, error) { block, rest := pem.Decode(echKey) if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { - return errors.New("invalid ECH keys pem") + return nil, errors.New("invalid ECH keys pem") } echKeys, err := UnmarshalECHKeys(block.Bytes) if err != nil { - return fmt.Errorf("parse ECH keys: %w", err) + return nil, fmt.Errorf("parse ECH keys: %w", err) } - tlsConfig.EncryptedClientHelloKeys = echKeys - return nil + return echKeys, err } diff --git a/mihomo/component/tls/utls.go b/mihomo/component/tls/utls.go index 2b33d323dd..d8ca9716c5 100644 --- a/mihomo/component/tls/utls.go +++ b/mihomo/component/tls/utls.go @@ -1,7 +1,10 @@ package tls import ( + "context" "net" + "reflect" + "unsafe" "github.com/metacubex/mihomo/common/once" "github.com/metacubex/mihomo/common/utils" @@ -126,8 +129,11 @@ type EncryptedClientHelloKey = utls.EncryptedClientHelloKey type Config = utls.Config +var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset +var tlsClientHelloInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.ClientHelloInfo)(nil)).Elem().FieldByName("ctx")).Offset + func UConfig(config *tls.Config) *utls.Config { - return &utls.Config{ + cfg := &utls.Config{ Rand: config.Rand, Time: config.Time, Certificates: utils.Map(config.Certificates, UCertificate), @@ -147,6 +153,52 @@ func UConfig(config *tls.Config) *utls.Config { SessionTicketsDisabled: config.SessionTicketsDisabled, Renegotiation: utls.RenegotiationSupport(config.Renegotiation), } + if config.GetClientCertificate != nil { + cfg.GetClientCertificate = func(info *utls.CertificateRequestInfo) (*utls.Certificate, error) { + tlsInfo := &tls.CertificateRequestInfo{ + AcceptableCAs: info.AcceptableCAs, + SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme { + return tls.SignatureScheme(it) + }), + Version: info.Version, + } + *(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsCertificateRequestInfoCtxOffset)) = info.Context() // for tlsInfo.ctx + cert, err := config.GetClientCertificate(tlsInfo) + if err != nil { + return nil, err + } + uCert := UCertificate(*cert) + return &uCert, err + } + } + if config.GetCertificate != nil { + cfg.GetCertificate = func(info *utls.ClientHelloInfo) (*utls.Certificate, error) { + tlsInfo := &tls.ClientHelloInfo{ + CipherSuites: info.CipherSuites, + ServerName: info.ServerName, + SupportedCurves: utils.Map(info.SupportedCurves, func(it utls.CurveID) tls.CurveID { + return tls.CurveID(it) + }), + SupportedPoints: info.SupportedPoints, + SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme { + return tls.SignatureScheme(it) + }), + SupportedProtos: info.SupportedProtos, + SupportedVersions: info.SupportedVersions, + Extensions: info.Extensions, + Conn: info.Conn, + //HelloRetryRequest: info.HelloRetryRequest, + } + *(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsClientHelloInfoCtxOffset)) = info.Context() // for tlsInfo.ctx + cert, err := config.GetCertificate(tlsInfo) + if err != nil { + return nil, err + } + uCert := UCertificate(*cert) + return &uCert, err + } + } + return cfg } // BuildWebsocketHandshakeState it will only send http/1.1 in its ALPN. diff --git a/mihomo/config/config.go b/mihomo/config/config.go index 301126a61e..c2b5664d3d 100644 --- a/mihomo/config/config.go +++ b/mihomo/config/config.go @@ -1292,7 +1292,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro } kLower := strings.ToLower(k) if strings.Contains(kLower, ",") { - if strings.Contains(kLower, "geosite:") { + if strings.HasPrefix(kLower, "geosite:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1300,7 +1300,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro newKey := "geosite:" + subkey policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } - } else if strings.Contains(kLower, "rule-set:") { + } else if strings.HasPrefix(kLower, "rule-set:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1315,9 +1315,9 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro } } } else { - if strings.Contains(kLower, "geosite:") { + if strings.HasPrefix(kLower, "geosite:") { policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) - } else if strings.Contains(kLower, "rule-set:") { + } else if strings.HasPrefix(kLower, "rule-set:") { policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) } else { policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) @@ -1712,7 +1712,7 @@ func parseSniffer(snifferRaw RawSniffer, ruleProviders map[string]P.RuleProvider } snifferConfig.SkipSrcAddress = skipSrcAddress - skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-src-address", ruleProviders) + skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-dst-address", ruleProviders) if err != nil { return nil, fmt.Errorf("error in skip-dst-address, error:%w", err) } @@ -1731,7 +1731,7 @@ func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string var matcher C.IpMatcher for _, ipcidr := range addresses { ipcidrLower := strings.ToLower(ipcidr) - if strings.Contains(ipcidrLower, "geoip:") { + if strings.HasPrefix(ipcidrLower, "geoip:") { subkeys := strings.Split(ipcidr, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1742,7 +1742,7 @@ func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string } matchers = append(matchers, matcher) } - } else if strings.Contains(ipcidrLower, "rule-set:") { + } else if strings.HasPrefix(ipcidrLower, "rule-set:") { subkeys := strings.Split(ipcidr, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1778,7 +1778,7 @@ func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapte var matcher C.DomainMatcher for _, domain := range domains { domainLower := strings.ToLower(domain) - if strings.Contains(domainLower, "geosite:") { + if strings.HasPrefix(domainLower, "geosite:") { subkeys := strings.Split(domain, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1789,7 +1789,7 @@ func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapte } matchers = append(matchers, matcher) } - } else if strings.Contains(domainLower, "rule-set:") { + } else if strings.HasPrefix(domainLower, "rule-set:") { subkeys := strings.Split(domain, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") diff --git a/mihomo/hub/route/server.go b/mihomo/hub/route/server.go index 6c47d67294..4e0d0c93f5 100644 --- a/mihomo/hub/route/server.go +++ b/mihomo/hub/route/server.go @@ -191,7 +191,7 @@ func startTLS(cfg *Config) { // handle tlsAddr if len(cfg.TLSAddr) > 0 { - cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(cfg.Certificate, cfg.PrivateKey) if err != nil { log.Errorln("External controller tls listen error: %s", err) return @@ -206,7 +206,9 @@ func startTLS(cfg *Config) { log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) tlsConfig := &tls.Config{Time: ntp.Now} tlsConfig.NextProtos = []string{"h2", "http/1.1"} - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(cfg.ClientAuthType) if len(cfg.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { @@ -214,7 +216,7 @@ func startTLS(cfg *Config) { } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(cfg.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(cfg.ClientAuthCert) if err != nil { log.Errorln("External controller tls listen error: %s", err) return @@ -223,7 +225,7 @@ func startTLS(cfg *Config) { } if cfg.EchKey != "" { - err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(cfg.EchKey, tlsConfig) if err != nil { log.Errorln("External controller tls serve error: %s", err) return diff --git a/mihomo/listener/anytls/server.go b/mihomo/listener/anytls/server.go index 731f13947e..0d35d4d6af 100644 --- a/mihomo/listener/anytls/server.go +++ b/mihomo/listener/anytls/server.go @@ -45,14 +45,16 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tls.Config{Time: ntp.Now} if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -65,7 +67,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } @@ -108,7 +110,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - if len(tlsConfig.Certificates) > 0 { + if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else { return nil, errors.New("disallow using AnyTLS without certificates config") diff --git a/mihomo/listener/http/server.go b/mihomo/listener/http/server.go index 2aba6fda4d..2c537dedd0 100644 --- a/mihomo/listener/http/server.go +++ b/mihomo/listener/http/server.go @@ -71,14 +71,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -91,14 +93,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -112,7 +114,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } diff --git a/mihomo/listener/mixed/mixed.go b/mihomo/listener/mixed/mixed.go index 995822b0cf..bc67a476e2 100644 --- a/mihomo/listener/mixed/mixed.go +++ b/mihomo/listener/mixed/mixed.go @@ -67,14 +67,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -87,14 +89,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -108,7 +110,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } diff --git a/mihomo/listener/sing_hysteria2/server.go b/mihomo/listener/sing_hysteria2/server.go index becb06b115..493607d525 100644 --- a/mihomo/listener/sing_hysteria2/server.go +++ b/mihomo/listener/sing_hysteria2/server.go @@ -56,15 +56,17 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi sl = &Listener{false, config, nil, nil} - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } tlsConfig := &tls.Config{ Time: ntp.Now, MinVersion: tls.VersionTLS13, } - tlsConfig.Certificates = []tls.Certificate{cert} + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) + if err != nil { + return nil, err + } + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { @@ -72,7 +74,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } @@ -80,7 +82,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } diff --git a/mihomo/listener/sing_vless/server.go b/mihomo/listener/sing_vless/server.go index 049f5eb1d9..83ba577924 100644 --- a/mihomo/listener/sing_vless/server.go +++ b/mihomo/listener/sing_vless/server.go @@ -81,14 +81,16 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -101,14 +103,14 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -153,7 +155,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if sl.decryption == nil { return nil, errors.New("disallow using Vless without any certificates/reality/decryption config") diff --git a/mihomo/listener/sing_vmess/server.go b/mihomo/listener/sing_vmess/server.go index 956aa70871..7dd0a163ba 100644 --- a/mihomo/listener/sing_vmess/server.go +++ b/mihomo/listener/sing_vmess/server.go @@ -81,14 +81,16 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -101,14 +103,14 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -153,7 +155,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } sl.listeners = append(sl.listeners, l) diff --git a/mihomo/listener/socks/tcp.go b/mihomo/listener/socks/tcp.go index 55e9e59439..45de213530 100644 --- a/mihomo/listener/socks/tcp.go +++ b/mihomo/listener/socks/tcp.go @@ -66,14 +66,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -86,14 +88,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -107,7 +109,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } diff --git a/mihomo/listener/trojan/server.go b/mihomo/listener/trojan/server.go index 6155d20927..e3c1002c9f 100644 --- a/mihomo/listener/trojan/server.go +++ b/mihomo/listener/trojan/server.go @@ -76,14 +76,16 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } @@ -96,14 +98,14 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { + if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { @@ -148,7 +150,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } if realityBuilder != nil { l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { + } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if !config.TrojanSSOption.Enabled { return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") diff --git a/mihomo/listener/tuic/server.go b/mihomo/listener/tuic/server.go index 30845515e7..7e659e596a 100644 --- a/mihomo/listener/tuic/server.go +++ b/mihomo/listener/tuic/server.go @@ -49,15 +49,17 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( return nil, err } - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } tlsConfig := &tls.Config{ Time: ntp.Now, MinVersion: tls.VersionTLS13, } - tlsConfig.Certificates = []tls.Certificate{cert} + certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) + if err != nil { + return nil, err + } + tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return certLoader() + } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { @@ -65,7 +67,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { - pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } @@ -73,7 +75,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( } if config.EchKey != "" { - err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } diff --git a/nodepass/.github/copilot-instructions.md b/nodepass/.github/copilot-instructions.md index 41ee779c86..9c89964730 100644 --- a/nodepass/.github/copilot-instructions.md +++ b/nodepass/.github/copilot-instructions.md @@ -1,96 +1,70 @@ -# NodePass AI Coding Agent Instructions +# NodePass Development Guide -## Project Overview +## Architecture Overview -NodePass is an enterprise-grade TCP/UDP network tunneling solution with a three-tier S/C/M architecture supporting server, client, and master modes. Written in Go 1.25+, focused on performance, security, and zero-configuration deployment. +NodePass is a Go-based TCP/UDP tunneling solution with a **tri-modal architecture** (Server/Client/Master) built on separation of control and data channels. -## Architecture Essentials +### Core Components -### Three-Tier S/C/M Architecture +- **`cmd/nodepass/`**: Entry point with URL-based configuration parsing + - `main.go`: Simple entry that invokes `start()` with version injection + - `core.go`: URL parser, logger initialization, TLS mode selection, core factory (`createCore()`) +- **`internal/`**: Three operational modes sharing `common.go` base (~1970 lines): + - `server.go`: Accepts tunnel connections via `tunnelHandshake()`, binds target addresses, supports bidirectional data flow + - `client.go`: Initiates tunnel connections, supports single-end forwarding (`singleStart()`) and dual-end handshake (`commonStart()`) + - `master.go`: RESTful API server with instance management, SSE events, gob persistence (~2165 lines) + - `common.go`: Shared functionality - DNS caching, buffer pools, slot management, connection routing +- **External packages** (NodePassProject org on GitHub): + - `pool`: TCP connection pooling with auto-scaling (min/max capacity) + - `quic`: QUIC transport with 0-RTT support + - `npws`: WebSocket transport adapter + - `conn`: Utilities (`DataExchange`, `StatConn` for traffic accounting, `RateLimiter` for bandwidth control) + - `logs`: Structured logger with levels (none/debug/info/warn/error/event) + - `cert`: TLS certificate generation and management -1. **Server Mode** (`internal/server.go`): Accepts tunnel connections, manages connection pools, forwards traffic bidirectionally -2. **Client Mode** (`internal/client.go`): Connects to servers, supports single-end forwarding or dual-end handshake modes -3. **Master Mode** (`internal/master.go`): RESTful API for dynamic instance management with persistent state in `nodepass.gob` +### Data Flow Modes -### Critical Design Patterns +1. **Server Receives Mode** (Reverse): Server binds target address locally → signals client → client connects back → data flows: External → Server → Client → Target +2. **Server Sends Mode** (Forward): Server connects to remote target → client signals server → server creates outgoing connection → data flows: Client → Server → Remote Target +3. **Client Single-End Forwarding**: Client binds tunnel address locally (e.g., `127.0.0.1:8080`) → direct forwarding to target without server coordination (no control channel) -- **Separation of Control/Data Channels**: - - Control channel: Unencrypted TCP for signaling (`np://` scheme with fragments) - - Data channel: Configurable TLS (modes 0/1/2) for actual traffic - -- **Connection Pooling**: Pre-established connections via `github.com/NodePassProject/pool` library - - Server controls `max` pool capacity, passes to client during handshake - - Client manages `min` capacity for persistent connections - - QUIC multiplexing available as alternative transport (`quic=1`) - -- **Bidirectional Data Flow**: Automatic mode detection in `Common.runMode` - - Mode 0: Auto-detect based on target address bindability - - Mode 1: Reverse/single-end (server receives OR client listens locally) - - Mode 2: Forward/dual-end (server sends OR client connects remotely) +Mode selection is **automatic** via `initTargetListener()` success/failure. Server tries binding target address; if successful = mode 1 (reverse), if fails = mode 2 (forward). Client tries binding tunnel address; if successful = single-end, if fails = dual-end. Force with `mode` query parameter (`0`=auto, `1`=reverse/single, `2`=forward/dual). -### Key Components -- `/cmd/nodepass/main.go`: Entry point, version variable injection -- `/cmd/nodepass/core.go`: Mode dispatch, TLS setup, certificate hot-reload -- `/internal/common.go`: Shared primitives (buffer pools, slot management, DNS resolution, encoding) -- `/internal/{server,client,master}.go`: Mode-specific implementations inheriting `Common` +## URL-Based Configuration -### External Dependencies (NodePassProject Ecosystem) +All configuration through URL scheme: `://@/?` -All critical networking primitives are in separate libraries: -- `github.com/NodePassProject/cert`: TLS certificate generation and management -- `github.com/NodePassProject/conn`: Enhanced connections (`StatConn` with traffic tracking) -- `github.com/NodePassProject/logs`: Multi-level logger (None/Debug/Info/Warn/Error/Event) -- `github.com/NodePassProject/name`: DNS resolver with caching and background refresh -- `github.com/NodePassProject/pool`: TCP connection pooling with auto-scaling -- `github.com/NodePassProject/quic`: QUIC multiplexing for 0-RTT connections - -**Never modify these libraries directly** - they're external dependencies. Use their exported APIs only. - -## Configuration System - -### URL-Based Configuration - -All modes use URL-style configuration: `scheme://[password@]host:port/target?param=value` - -```bash -# Server: bind_addr/target_addr with pool capacity and TLS -server://password@0.0.0.0:10101/127.0.0.1:8080?max=1024&tls=1&log=debug - -# Client: server_addr/local_addr with min capacity and mode -client://password@server:10101/127.0.0.1:9090?min=128&mode=0&rate=100 - -# Master: api_addr/prefix with TLS and custom certs -master://0.0.0.0:9090/api?log=info&tls=2&crt=/path/cert.pem&key=/path/key.pem +**URL Structure Examples:** +``` +server://password@0.0.0.0:10101/127.0.0.1:8080?tls=1&max=512 +client://password@server.com:10101/localhost:8080?min=64&type=1 +master://0.0.0.0:9090/api?log=debug&tls=2&crt=/path/cert.pem&key=/path/key.pem ``` -### Query Parameters +**Critical query parameters:** +- `log`: Log level - `none`|`debug`|`info`(default)|`warn`|`error`|`event` +- `tls`: Encryption mode - `0` (plain TCP/UDP), `1` (self-signed cert in memory), `2` (custom cert with `crt`/`key` files) + - Mode 0: No encryption, fastest but insecure + - Mode 1: Auto-generated self-signed cert, no verification, protects against passive sniffing + - Mode 2: Custom certificate with validation, requires both `crt` and `key` parameters pointing to PEM files + - **Note**: QUIC transport (`type=1`) requires minimum `tls=1` +- `type`: Pool transport protocol - `0` (TCP pool, default), `1` (QUIC with 0-RTT), `2` (WebSocket) +- `mode`: Force run mode - `0` (auto-detect via binding), `1` (server=reverse/client=single-end), `2` (server=forward/client=dual-end) +- `dns`: DNS cache TTL duration (default `5m`, accepts Go duration syntax like `30s`, `10m`, `1h`) +- `min`: Client minimum pool capacity (default `64`) +- `max`: Server maximum pool capacity (default `1024`) +- `rate`: Bandwidth limit in **Mbps * 8** (e.g., `rate=100` = 100Mbps = 12.5MB/s; internal unit is bytes/sec, computed as rate*125000) +- `slot`: Max concurrent connections - TCP+UDP combined (default `65536`, `0`=unlimited) +- `proxy`: PROXY protocol version - `0` (disabled), `1` (v1 text format), `2` (v2 binary format) +- `read`: Connection read timeout (default `0` = infinite, accepts Go duration like `30s`, `5m`) +- `dial`: Local bind IP for outgoing connections (default `auto` = system routing, or specific IP like `192.168.1.100`) + - Automatic fallback to system routing if specified IP fails (logged as "fallback to system auto") +- `notcp`: Disable TCP forwarding - `0` (enabled), `1` (disabled) +- `noudp`: Disable UDP forwarding - `0` (enabled), `1` (disabled) -- `log`: none|debug|info|warn|error|event (default: info) -- `tls`: 0=plain, 1=self-signed, 2=custom cert (server/master only, client inherits from server) -- `min`/`max`: Connection pool capacity (client sets min, server sets max) -- `mode`: 0=auto, 1=reverse/single-end, 2=forward/dual-end -- `quic`: 0=TCP pool, 1=QUIC multiplexing (requires tls≥1) -- `dns`: Custom DNS servers (comma-separated, default: 1.1.1.1,8.8.8.8) -- `read`: Timeout duration (e.g., 1h, 30m, 15s, default: 0=no timeout) -- `rate`: Mbps bandwidth limit (0=unlimited) -- `slot`: Max concurrent connections (default: 65536) -- `proxy`: PROXY protocol v1 support (0=off, 1=on) -- `dial`: Local bind IP for outbound connections (default: auto) -- `notcp`/`noudp`: Disable TCP/UDP (0=enabled, 1=disabled) +**Password field usage:** The `@` password portion in URLs (e.g., `mykey@server:10101`) becomes `tunnelKey` for authentication - it's NOT a system password, just a shared secret for tunnel validation. Server compares incoming `tunnelKey` via XOR+base64 encoding in handshake. -### Environment Variables for Tuning - -Runtime behavior tunable without recompilation (see `internal/common.go`): -```go -NP_TCP_DATA_BUF_SIZE=16384 // TCP buffer size -NP_UDP_DATA_BUF_SIZE=16384 // UDP buffer size -NP_HANDSHAKE_TIMEOUT=5s // Handshake timeout -NP_POOL_GET_TIMEOUT=5s // Pool connection acquisition timeout -NP_REPORT_INTERVAL=5s // Health check reporting interval -NP_RELOAD_INTERVAL=1h // TLS cert hot-reload interval (mode 2) -NP_SEMAPHORE_LIMIT=65536 // Signal channel buffer size -NP_DNS_CACHING_TTL=5m // DNS cache TTL -``` +Examples in `docs/en/examples.md`, full configuration reference in `docs/en/configuration.md`. ## Development Workflow @@ -98,199 +72,439 @@ NP_DNS_CACHING_TTL=5m // DNS cache TTL ```bash # Development build -go build -o nodepass ./cmd/nodepass +cd cmd/nodepass +go build -ldflags "-X main.version=dev" -# Release build with version injection (mimics .goreleaser.yml) -go build -trimpath -ldflags="-s -w -X main.version=1.0.0" -o nodepass ./cmd/nodepass -``` +# Release build (via goreleaser) +goreleaser build --snapshot --clean -### Testing Manually - -No automated test suite exists currently. Test via real-world scenarios: - -```bash -# Terminal 1: Server with debug logging and self-signed TLS -./nodepass "server://0.0.0.0:10101/127.0.0.1:8080?log=debug&tls=1&max=256" - -# Terminal 2: Client connecting to server -./nodepass "client://localhost:10101/127.0.0.1:9090?log=debug&min=64" - -# Terminal 3: Master API mode -./nodepass "master://0.0.0.0:9090/api?log=debug&tls=0" -``` - -**Test checklist** (from CONTRIBUTING.md): -1. Test each mode (server, client, master) with `log=debug` -2. Verify TCP and UDP forwarding separately -3. Test all TLS modes (0, 1, 2) with certificate validation -4. Test QUIC mode (`quic=1`) with TLS≥1 -5. Verify graceful shutdown with SIGTERM/SIGINT -6. Stress test with high concurrency and connection pool scaling - -### Docker Build - -```bash +# Docker build (multi-stage, scratch-based final image) docker build --build-arg VERSION=dev -t nodepass:dev . ``` -### Release Process +Build produces single static binary with no external dependencies. The `-ldflags "-X main.version=..."` injects version into `main.version` variable displayed in `exit()` banner. -Uses GoReleaser on tag push (`v*.*.*`). See `.goreleaser.yml` for build matrix (Linux, Windows, macOS, FreeBSD across multiple architectures). +### Testing Patterns -## Code Patterns and Conventions +**No test suite exists** - all testing is manual via URL invocations. Common test scenarios: -### Error Handling +```bash +# Server mode (binds :10101 for tunnel, forwards to local 8080) +nodepass "server://:10101/127.0.0.1:8080?log=debug&tls=1" -Always wrap errors with context using `fmt.Errorf("function: action failed: %w", err)`. See pattern in `start()`, `createCore()`, `NewServer()`, etc. +# Client mode (connects to server:10101, creates local listener on :8080) +nodepass "client://server:10101/127.0.0.1:8080?min=128&log=debug" + +# Master API mode (launches API server on :10101 with /api prefix) +nodepass "master://:10101/api?log=debug&tls=1" + +# Test QUIC transport with bandwidth limiting +nodepass "server://:10101/127.0.0.1:8080?type=1&tls=1&rate=100" + +# Test multi-target load balancing (comma-separated targets) +nodepass "client://server:10101/target1.com:80,target2.com:80,target3.com:80?mode=2" +``` + +**Debugging tips:** +- Use `log=debug` to see connection lifecycle events, pool operations, handshake details +- Check `DataExchange` log messages for connection completion status and byte counts +- Monitor pool capacity with `Active()` and `Capacity()` calls logged periodically +- TLS handshake failures appear as "access denied" warnings - verify `tunnelKey` matches +- DNS resolution issues trigger fallback to cached addresses with warning logs + +### Environment Tuning + +Performance constants in `common.go` (lines 93-105) are environment-configurable via `NP_*` prefix: + +```bash +# Increase semaphore limit for high concurrency (default 65536) +export NP_SEMAPHORE_LIMIT=131072 + +# Larger TCP buffer for high-bandwidth links (default 16384) +export NP_TCP_DATA_BUF_SIZE=32768 + +# Extend handshake timeout for slow networks (default 5s) +export NP_HANDSHAKE_TIMEOUT=10s + +# Pool connection acquisition timeout (default 5s) +export NP_POOL_GET_TIMEOUT=10s + +# Pool scaling intervals (defaults: min=100ms, max=1s) +export NP_MIN_POOL_INTERVAL=50ms +export NP_MAX_POOL_INTERVAL=2s + +# Health check report frequency (default 5s) +export NP_REPORT_INTERVAL=10s + +# Service restart cooldown (default 3s) +export NP_SERVICE_COOLDOWN=5s + +# Graceful shutdown timeout (default 5s) +export NP_SHUTDOWN_TIMEOUT=10s + +# TLS certificate reload interval for mode 2 (default 1h) +export NP_RELOAD_INTERVAL=30m +``` + +All duration values accept Go duration syntax (`s`, `m`, `h`). Changes require restart to take effect. + +## Code Conventions ### Logging -Use the injected `logger` instance with appropriate levels: +Use structured logging with `logger` from `logs.Logger`. Six levels: none/debug/info/warn/error/event. Format strings with `%v` placeholders: + ```go -logger.Debug("Detailed info: %v", detail) // Verbose debugging -logger.Info("Operation: %v", status) // Normal operations -logger.Warn("Non-critical issue: %v", err) // Recoverable problems -logger.Error("Critical error: %v", err) // Functionality affected -logger.Event("Traffic stats: %v", stats) // Important events +logger.Debug("TLS cert reloaded: %v", crtFile) +logger.Info("Server started: server://%v@%v/%v", key, tunnel, target) +logger.Warn("tunnelHandshake: access denied: %v", remoteAddr) +logger.Error("Certificate load failed: %v", err) +logger.Event("Traffic stats: TCP RX=%d TX=%d", tcpRX, tcpTX) ``` -### Goroutine Management +**Never use `fmt.Printf`** except in `exit()` help banner. All user-facing output goes through logger. -All long-running goroutines must: -1. Check `ctx.Err()` regularly for cancellation -2. Use proper cleanup with `defer` statements -3. Handle panics in critical sections -4. Release resources (slots, buffers, connections) on exit +### Error Handling -### Buffer Management +Wrap errors with context using `fmt.Errorf` with `%w` verb for error chain preservation: -Use sync.Pool for TCP/UDP buffers to reduce GC pressure: ```go -buf := c.getTCPBuffer() // Gets []byte from tcpBufferPool -defer c.putTCPBuffer(buf) +return fmt.Errorf("start: initTunnelListener failed: %w", err) +return fmt.Errorf("tunnelHandshake: decode failed: %w", err) ``` +Functions return `error` as last return value. Restart logic uses `err != nil && err != io.EOF` pattern - `io.EOF` signals graceful shutdown, other errors trigger restart after `serviceCooldown`. + +### Connection Pool Interface + +All transport types (`pool.ServerPool`, `quic.ServerPool`, `npws.ServerPool`) implement unified `TransportPool` interface (defined in `common.go` line 92): + +```go +type TransportPool interface { + // IncomingGet retrieves connection from server pool by ID with timeout + IncomingGet(timeout time.Duration) (string, net.Conn, error) + + // OutgoingGet retrieves connection from client pool for given ID with timeout + OutgoingGet(id string, timeout time.Duration) (net.Conn, error) + + // Flush signals pool to drop all connections and reset state + Flush() + + // Close terminates pool and all managed connections + Close() + + // Ready reports if pool has reached minimum capacity + Ready() bool + + // Active returns current active connection count + Active() int + + // Capacity returns maximum pool capacity + Capacity() int + + // Interval returns current auto-scaling interval + Interval() time.Duration + + // AddError increments error counter for health monitoring + AddError() + + // ErrorCount returns cumulative error count + ErrorCount() int + + // ResetError clears error counter + ResetError() +} +``` + +Connection IDs are generated via FNV hash: `hash := fnv.New64a(); hash.Write([]byte); id := hex.EncodeToString(hash.Sum(nil))`. Server generates IDs for incoming connections, client receives IDs via control channel. + +### Buffer Pool Management + +**Critical**: Always return buffers to prevent memory leaks. Pools are initialized in constructor with `sync.Pool`: + +```go +tcpBufferPool: &sync.Pool{ + New: func() any { + buf := make([]byte, tcpDataBufSize) + return &buf + }, +} +``` + +Usage pattern: +```go +buffer := c.getTCPBuffer() // Acquire from pool +defer c.putTCPBuffer(buffer) // ALWAYS return via defer +// Use buffer for I/O operations... +``` + +UDP buffers follow identical pattern with `getUDPBuffer()`/`putUDPBuffer()`. Buffer sizes configurable via `NP_TCP_DATA_BUF_SIZE` (default 16384) and `NP_UDP_DATA_BUF_SIZE` (default 16384). + ### Slot Management -Connection slots prevent resource exhaustion: +Connection slots limit concurrent connections via atomic counters. Check before accepting connections: + ```go if !c.tryAcquireSlot(isUDP) { - return fmt.Errorf("slot limit reached") + logger.Warn("Slot limit reached: %d", c.slotLimit) + conn.Close() + return } defer c.releaseSlot(isUDP) ``` -### Configuration via Environment Variables +Slots are combined TCP+UDP count. `slotLimit=0` disables limit. Slot tracking uses `atomic.AddInt32()` for thread-safe counters. + +### Context Management + +Each mode initializes context in `start()` method: -Runtime behavior tunable without recompilation: ```go -var tcpDataBufSize = getEnvAsInt("NP_TCP_DATA_BUF_SIZE", 16384) -``` -See `common.go` for full list: `NP_SEMAPHORE_LIMIT`, `NP_HANDSHAKE_TIMEOUT`, `NP_POOL_GET_TIMEOUT`, etc. - -### TLS Certificate Hot-Reload - -Mode 2 (custom certs) reloads certificates hourly without restart using `GetCertificate` callback in `core.go`. - -### Comments Style - -Maintain bilingual (Chinese/English) comments for public APIs and exported functions: -```go -// NewServer 创建新的服务端实例 -// NewServer creates a new server instance -func NewServer(parsedURL *url.URL, ...) (*Server, error) { ... } +func (c *Common) initContext() { + c.ctx, c.cancel = context.WithCancel(context.Background()) +} ``` -## External Dependencies +Graceful shutdown via `shutdown(ctx, stopFunc)` helper: +1. Calls `stopFunc()` to close listeners/pools +2. Waits for `ctx.Done()` or `shutdownTimeout` (default 5s) +3. Logs completion/timeout status -All from `github.com/NodePassProject/*` ecosystem: -- **cert**: TLS certificate generation and management -- **conn**: Enhanced network connections with statistics tracking (`StatConn`) -- **logs**: Multi-level logger (None/Debug/Info/Warn/Error/Event) -- **name**: DNS resolver with caching and background refresh -- **pool**: TCP connection pooling with auto-scaling -- **quic**: QUIC multiplexing for 0-RTT connections +Restart loop pattern in `Run()` methods: +```go +for ctx.Err() == nil { + if err := c.start(); err != nil && err != io.EOF { + c.logger.Error("Client error: %v", err) + c.stop() + select { + case <-ctx.Done(): + return + case <-time.After(serviceCooldown): // 3s default + } + logInfo("Client restart") + } +} +``` + +Use `contextCheckInterval` (50ms) in tight loops: `select { case <-ctx.Done(): return; case <-time.After(contextCheckInterval): }` + +### Traffic Accounting + +All connections wrapped in `conn.StatConn` for automatic byte counting and rate limiting: + +```go +targetConn = &conn.StatConn{ + Conn: targetConn, + RX: &c.tcpRX, // Points to Common's atomic uint64 counter + TX: &c.tcpTX, // Points to Common's atomic uint64 counter + Rate: c.rateLimiter, // Optional rate limiter (nil if rate=0) +} +``` + +Counters updated atomically on every Read/Write. Master mode reads counters to compute traffic deltas. `DataExchange()` from `conn` package handles bidirectional copy with automatic accounting: + +```go +conn.DataExchange(connA, connB, readTimeout, buffer1, buffer2) +``` + +Rate limiting initialized via `initRateLimiter()` if `rateLimit > 0` (rate in bytes/sec = query param * 125000). ## Master Mode Specifics -### API Patterns - -- Authentication via `X-API-Key` header (auto-generated, stored in `nodepass.gob`) -- SSE events at `/events` endpoint for real-time updates -- State persistence with `encoding/gob` for instance recovery -- OpenAPI spec at `/openapi.json`, Swagger UI at `/docs` - -RESTful endpoints at `/{prefix}/*` (default `/api/*`): -- Instance CRUD: POST/GET/PATCH/PUT/DELETE on `/instances` and `/instances/{id}` -- Real-time events: SSE stream at `/events` (types: initial, create, update, delete, shutdown, log) -- Service info: GET/POST on `/info` for master details and alias updates -- TCPing utility: GET on `/tcping` for connection testing - ### Instance Management -Each instance runs as a separate `exec.Cmd` process. Master tracks via `instances sync.Map` with status fields: `running`, `stopped`, `error`. Auto-restart enabled via `Restart` boolean field. +Instances stored in `sync.Map` (concurrent-safe), persisted to `gob/nodepass.gob` using `gob` encoding. State file layout: +- API key (auto-generated 32-byte hex on first start) +- Instance map serialization with all fields except those tagged `gob:"-"` -### State Persistence +Key `Instance` struct fields: +```go +type Instance struct { + ID string // 8-char hex identifier + Alias string // User-friendly name + Type string // "server" or "client" + Status string // "running", "stopped", "error" + URL string // Original user-provided URL + Config string // Computed URL with all defaults filled + Restart bool // Auto-restart policy + Meta Meta // Metadata with peer info and tags + cmd *exec.Cmd // Running subprocess (not serialized) + stopped chan struct{} // Shutdown coordination (not serialized) + // Traffic baseline tracking (not serialized) + TCPRXBase/TCPTXBase/UDPRXBase/UDPTXBase uint64 +} +``` -All instances stored in `nodepass.gob` using Go's `encoding/gob`: -- Auto-saved on instance changes via `saveMasterState()` -- Restored on startup via `restoreMasterState()` -- Mutex-protected writes with `stateMu` +Instance lifecycle: +1. **Create**: `POST /instances` with URL → generates ID → spawns subprocess → stores in `sync.Map` → persists to gob +2. **Monitor**: Periodic goroutine reads `/proc//status` for traffic stats, computes deltas from baseline +3. **Update**: `PATCH /instances/{id}` with actions: `start`, `stop`, `restart`, `reset-traffic`, `toggle-restart` +4. **Delete**: `DELETE /instances/{id}` → stops subprocess → removes from map → re-persists gob + +Subprocess management uses `exec.CommandContext()` with instance-specific context. Logs captured via custom `InstanceLogWriter` that parses structured logs and emits SSE events. + +### SSE Events + +Real-time updates via `/events` endpoint (Server-Sent Events). Event types and payloads: + +- `initial`: Full instance list on connection (sent once per subscriber) +- `create`: New instance created (includes full Instance object) +- `update`: Instance state changed (includes full Instance object with updated fields) +- `delete`: Instance removed (includes ID only) +- `shutdown`: Master shutting down (no payload) +- `log`: Instance log line (includes `instance.id` and `logs` fields) + +Subscribers stored in `sync.Map` with unique IDs. Event broadcasting via `notifyChannel` (buffered channel). Connection management pattern: + +```go +subscriber := &Subscriber{id: generateID(), channel: make(chan *InstanceEvent, 100)} +m.subscribers.Store(subscriber.id, subscriber) +defer m.subscribers.Delete(subscriber.id) + +for { + select { + case event := <-subscriber.channel: + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, jsonData) + flusher.Flush() + case <-r.Context().Done(): + return + } +} +``` ### API Authentication -API Key in `X-API-Key` header. Special instance ID `********` for key regeneration via PATCH action `restart`. +Auto-generated API key on first start. Special instance ID `********` (8 asterisks) reserved for key operations: +- `GET /instances/********`: Retrieve current API key +- `PATCH /instances/********` with `{"action": "restart"}`: Regenerate API key -## Testing and Validation +Protected endpoints check `X-API-Key` header. Public endpoints: `/openapi.json`, `/docs` (Swagger UI). -No automated test suite currently. Manual testing workflow (from CONTRIBUTING.md): -1. Test each mode (server, client, master) with `log=debug` -2. Verify TCP and UDP forwarding separately -3. Test TLS modes 0, 1, 2 with certificate validation -4. Stress test with high concurrency and connection pool scaling +Key validation pattern: +```go +if apiKey := r.Header.Get("X-API-Key"); apiKey != m.apiKey { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return +} +``` + +### TCPing Functionality + +Built-in connectivity testing via `GET /tcping?target=host:port`. Concurrent limit enforced via buffered semaphore (`tcpingSem chan struct{}` with capacity 10). Returns JSON: + +```json +{ + "target": "example.com:443", + "connected": true, + "latency": 42, + "error": null +} +``` + +Timeout handling: 1s for semaphore acquisition, 5s for TCP dial. Latency measured in milliseconds. + +## Integration Points + +### External Package Boundaries + +- **`github.com/NodePassProject/pool`**: TCP connection pooling with dynamic scaling +- **`github.com/NodePassProject/quic`**: QUIC-based transport (0-RTT support) +- **`github.com/NodePassProject/npws`**: WebSocket transport wrapper +- **`github.com/NodePassProject/conn`**: Connection helpers (`DataExchange`, `StatConn`, `RateLimiter`) +- **`github.com/NodePassProject/cert`**: TLS certificate generation/management + +When modifying transport behavior, coordinate with corresponding package version in `go.mod`. + +### DNS Caching + +Custom DNS resolution via `dnsCacheEntry` stored in `sync.Map` with TTL. Functions: `getTunnelTCPAddr()`, `getTargetTCPAddr()`. + +### Handshake Protocol + +**Server-side handshake** (`server.go` lines 208-279): +1. Creates HTTP server with `HandlerFunc` on `tunnelListener` +2. Validates incoming HTTP GET request to path `/` +3. Extracts `Authorization` header and verifies Bearer token using HMAC-SHA256: + - Client sends: `Authorization: Bearer ` + - Server verifies via `hmac.Equal()` constant-time comparison +4. Extracts client IP from `RemoteAddr()` (strips port if present) +5. Responds with JSON config containing: + ```json + { + "flow": "", // Direction: "+" or "-" + "max": , // Server pool capacity + "tls": "", // TLS mode: "0", "1", or "2" + "type": "" // Transport: "0" (TCP), "1" (QUIC), "2" (WS) + } + ``` +6. Closes HTTP server after successful handshake +7. Recreates `tunnelListener` for subsequent pool connections + +**Client-side handshake** (`client.go` lines 231-273): +1. Constructs HTTP GET request to `http:///` +2. Sets `Host` header to `tunnelName` for DNS-based routing +3. Generates HMAC-SHA256 token: `hex.EncodeToString(hmac.New(sha256.New, []byte(tunnelKey)).Sum(nil))` +4. Sends `Authorization: Bearer ` header +5. Receives JSON response and decodes config +6. Updates local configuration: + - `dataFlow`: Controls connection direction + - `maxPoolCapacity`: Adopts server's pool size + - `tlsCode`: Applies server's TLS settings to data connections + - `poolType`: Switches transport type if needed +7. Logs loaded configuration for debugging + +**Authentication mechanism**: HMAC-SHA256 provides cryptographic authentication without transmitting the raw `tunnelKey`. Token generation in `common.go` lines 248-256 uses standard library `crypto/hmac` and `crypto/sha256`. + +### Load Balancing & Failover + +Multi-target support via comma-separated addresses in URL path. `dialWithRotation()` (`common.go` lines 385-450) implements: +- Round-robin distribution using atomic counter +- Automatic failover on connection errors +- Single-target fast path optimization +- Dynamic DNS resolution per attempt + +Example: `client://server:10101/target1:80,target2:80,target3:80` rotates across three backends. ## Common Pitfalls -- **URL parsing**: Always include scheme (`server://`, `client://`, `master://`) or startup fails -- **TLS mismatch**: Client inherits TLS mode from server during handshake—don't configure client TLS manually -- **Pool capacity**: Server sets `max`, client sets `min`—mismatch causes connection issues -- **Local address detection**: Single-end mode triggers automatically for localhost/127.0.0.1 tunnel addresses -- **QUIC requirement**: QUIC mode (`quic=1`) forces TLS mode 1 minimum—cannot use mode 0 -- **Don't modify NodePassProject libraries**: These are external dependencies, not internal packages -- **Always decode before using tunnel URLs**: Use `Common.decode()` for base64+XOR encoded data -- **UDP session cleanup**: Sessions in `targetUDPSession` require explicit cleanup with timeouts -- **Certificate hot-reload**: Only applies to `tls=2` mode with periodic checks every `ReloadInterval` -- **Graceful shutdown**: Use context cancellation propagation, don't abruptly close connections +1. **TLS Mode vs Pool Type**: `tls` parameter applies to data channel, `type` parameter selects transport (QUIC requires `tls=1` minimum) +2. **URL Password Field**: Used as `tunnelKey` for authentication - not actual password +3. **Buffer Pool Management**: Always return buffers via `putTCPBuffer()`/`putUDPBuffer()` to prevent leaks +4. **Signal Channel Buffering**: `signalChan` has `semaphoreLimit` capacity - blocks if full +5. **Instance Config vs URL**: Master stores both user-provided URL and computed config string with all defaults -## Key Files Reference +## Key File References -- `cmd/nodepass/main.go`: Entry point, version variable injection -- `cmd/nodepass/core.go`: Mode dispatch, TLS setup, CLI help formatting -- `internal/common.go`: Shared primitives (buffer pools, slot management, encoding, config init) -- `internal/server.go`: Server lifecycle, tunnel handshake, forward/reverse modes -- `internal/client.go`: Client lifecycle, single-end/dual-end modes, tunnel connection -- `internal/master.go`: HTTP API, SSE events, instance subprocess management, state persistence +- **`internal/common.go`** (1970 lines): Core shared functionality + - Lines 29-85: `Common` struct definition with all shared fields + - Lines 93-122: Environment-configurable performance constants + - Lines 140-165: Buffer pool management (`getTCPBuffer`, `putTCPBuffer`, `getUDPBuffer`, `putUDPBuffer`) + - Lines 168-200: Slot management (`tryAcquireSlot`, `releaseSlot`) + - Lines 250-270: Handshake encoding/decoding (`xor`, `encode`, `decode`) + - Lines 385-450: Load balancing with failover (`dialWithRotation`) + - Lines 722-726: Rate limiter initialization + - Lines 1229, 1568: `DataExchange` calls for bidirectional traffic -## Documentation Requirements +- **`internal/server.go`** (320 lines): Server mode implementation + - Lines 32-62: Server constructor with pool initialization + - Lines 65-106: Run loop with restart logic + - Lines 109-183: Start sequence and mode detection + - Lines 194-320: Tunnel handshake with concurrent connection acceptance -When adding features: -1. Update relevant `docs/en/*.md` and `docs/zh/*.md` files -2. Add examples to `docs/en/examples.md` -3. Document new query parameters in `docs/en/configuration.md` -4. Update API endpoints in `docs/en/api.md` if touching master mode -5. Keep README.md feature list current +- **`internal/client.go`** (273 lines): Client mode implementation + - Lines 33-61: Client constructor + - Lines 111-132: Mode detection logic (single-end vs dual-end) + - Lines 135-210: Pool initialization per transport type + - Lines 218-273: Tunnel handshake with config reception -## Additional Notes +- **`internal/master.go`** (2165 lines): Master API server + - Lines 67-90: Master struct definition + - Lines 91-124: Instance struct with traffic tracking + - Lines 138-145: InstanceEvent for SSE + - Lines 330+: RESTful handlers and instance management -- Project uses Go 1.25+ features, maintain compatibility -- Single binary with no external runtime dependencies (except TLS cert files for mode 2) -- Focus on zero-configuration deployment - defaults should work for most use cases -- Performance-critical paths: buffer allocation, connection pooling, data transfer loops -- Security considerations: TLS mode selection, API key protection, input validation on master API - -## Documentation References - -- `/docs/en/how-it-works.md`: Deep dive into control/data channel separation and data flow modes -- `/docs/en/configuration.md`: Complete parameter reference with examples -- `/docs/en/api.md`: Master mode API specification with authentication and SSE events -- `CONTRIBUTING.md`: Development setup, architecture overview, contribution guidelines +- **`cmd/nodepass/core.go`** (165 lines): Entry point and configuration + - Lines 17-35: URL parsing and core creation + - Lines 38-59: Logger initialization + - Lines 62-75: Core factory (`createCore`) + - Lines 78-143: TLS configuration with three modes diff --git a/nodepass/README.md b/nodepass/README.md index f09002a893..da8cb3203e 100644 --- a/nodepass/README.md +++ b/nodepass/README.md @@ -25,7 +25,7 @@ English | [简体中文](README_zh.md) - Cross-platform, multi-architecture, single binary or container. - **🚀 Connection Pool** - - Dual transport options, TCP-based pools or QUIC-based multiplexing. + - Supports TCP, QUIC, WebSocket, HTTP/2 pooling transport methods. - Eliminates handshake delays, boosts performance with 0-RTT support. - Auto-scaling with real-time capacity adjustment. diff --git a/nodepass/README_zh.md b/nodepass/README_zh.md index 9b118dc3cc..be38d594ef 100644 --- a/nodepass/README_zh.md +++ b/nodepass/README_zh.md @@ -25,7 +25,7 @@ - 多平台、多架构支持,支持独立二进制文件、容器灵活部署。 - **🚀 内置连接池** - - 双传输选择,基于 TCP 的连接池或基于 QUIC 的多路复用。 + - 提供 TCP、QUIC、WebSocket、HTTP/2 多种池化传输方式。 - 消除连接的握手等待,通过 0-RTT 支持显著提升性能体验。 - 支持实时容量自适应,动态调整连接池规模。 diff --git a/nodepass/cmd/nodepass/core.go b/nodepass/cmd/nodepass/core.go index f54cd2732c..d59d5483a0 100644 --- a/nodepass/cmd/nodepass/core.go +++ b/nodepass/cmd/nodepass/core.go @@ -129,9 +129,9 @@ func getTLSProtocol(parsedURL *url.URL, logger *logs.Logger) (string, *tls.Confi } return "2", tlsConfig default: - if parsedURL.Query().Get("quic") == "1" { - // QUIC模式下不支持明文传输 - logger.Info("TLS code-1: RAM cert with TLS 1.3 for QUIC") + if poolType := parsedURL.Query().Get("type"); poolType == "1" || poolType == "3" { + // 流池类型不支持明文传输 + logger.Info("TLS code-1: RAM cert with TLS 1.3 for stream pool") return "1", tlsConfig } // 不使用加密 @@ -147,10 +147,6 @@ func exit(err error) { } fmt.Printf(` ╭─────────────────────────────────────╮ -│ ░░█▀█░█▀█░░▀█░█▀▀░█▀█░█▀█░█▀▀░█▀▀░░ │ -│ ░░█░█░█░█░█▀█░█▀▀░█▀▀░█▀█░▀▀█░▀▀█░░ │ -│ ░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀▀▀░░ │ -├─────────────────────────────────────┤ │%*s │ │%*s │ ├─────────────────────────────────────┤ @@ -159,6 +155,6 @@ func exit(err error) { │ master://hostname:port/path? │ ╰─────────────────────────────────────╯ -`, 36, version, 36, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)) +`, 36, fmt.Sprintf("nodepass-%s", version), 36, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)) os.Exit(1) } diff --git a/nodepass/docs/en/api.md b/nodepass/docs/en/api.md index b7b7818357..6952f580f9 100644 --- a/nodepass/docs/en/api.md +++ b/nodepass/docs/en/api.md @@ -65,7 +65,7 @@ API Key authentication is enabled by default, automatically generated and saved "type": "client|server", "status": "running|stopped|error", "url": "...", - "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&quic=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&type=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", "restart": true, "meta": { "peer": { @@ -111,7 +111,7 @@ API Key authentication is enabled by default, automatically generated and saved - Server: `server://:/:?` - Client: `client://:/:?` -- Supported parameters: `log`, `tls`, `crt`, `key`, `dns`, `min`, `max`, `mode`, `quic`, `dial`, `read`, `rate`, `slot`, `proxy`, `notcp`, `noudp` +- Supported parameters: `log`, `tls`, `crt`, `key`, `dns`, `min`, `max`, `mode`, `type`, `dial`, `read`, `rate`, `slot`, `proxy`, `notcp`, `noudp` ### URL Query Parameters @@ -123,7 +123,7 @@ API Key authentication is enabled by default, automatically generated and saved - `mode`: Runtime mode control (`0`, `1`, `2`) - Controls operation behavior - For server: `0`=auto, `1`=reverse mode, `2`=forward mode - For client: `0`=auto, `1`=single-end forwarding, `2`=dual-end handshake -- `quic`: QUIC transport mode (`0`=TCP pool, `1`=QUIC pool, default: `0`) - Server mode only, client receives configuration during handshake +- `type`: Connection pool type (`0`=TCP pool, `1`=QUIC pool, `2`=WebSocket pool, default: `0`) - Server mode only, client receives configuration during handshake - `dial`: Source IP for outbound connections (default: `auto`) - Server/Client mode only - `read`: Data read timeout duration (e.g., 1h, 30m, 15s, default: `0` for no timeout) - `rate`: Bandwidth rate limit in Mbps (0=unlimited) @@ -1116,7 +1116,7 @@ The instance object in API responses contains the following fields: "type": "server", // Instance type: server or client "status": "running", // Instance status: running, stopped, or error "url": "server://...", // Instance configuration URL - "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&quic=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", // Complete configuration URL + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&type=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", // Complete configuration URL "restart": true, // Auto-restart policy "meta": { // Metadata for organization and peer tracking "peer": { @@ -1335,7 +1335,7 @@ const instances = await fetch(`${API_URL}/instances`, { #### POST /instances - **Description**: Create new instance - **Authentication**: Requires API Key -- **Request body**: `{ "url": "client:// or server:// format URL" }` +- **Request body**: `{ "alias": "Instance alias (optional)", "url": "client:// or server:// format URL" }` - **Response**: Newly created instance object - **Example**: ```javascript @@ -1345,7 +1345,10 @@ const newInstance = await fetch(`${API_URL}/instances`, { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, - body: JSON.stringify({ url: 'server://0.0.0.0:8080/localhost:3000' }) + body: JSON.stringify({ + alias: 'My Server', + url: 'server://0.0.0.0:8080/localhost:3000' + }) }); ``` @@ -1582,14 +1585,16 @@ Examples: | `crt` | Certificate path | File path | None | Server only | | `key` | Private key path | File path | None | Server only | | `dns` | DNS cache duration | Time duration (e.g., `5m`, `30s`, `1h`) | `5m` | Both | +| `sni` | Server Name Indication | Hostname | `none` | Client dual-end handshake mode only | | `min` | Minimum pool capacity | Integer > 0 | `64` | Client dual-end handshake mode only | | `max` | Maximum pool capacity | Integer > 0 | `1024` | Dual-end handshake mode | | `mode` | Runtime mode control | `0`(auto), `1`(force mode 1), `2`(force mode 2) | `0` | Both | -| `quic` | QUIC protocol support | `0`(disabled), `1`(enabled) | `0` | Server only | +| `type` | Connection pool type | `0`(TCP), `1`(QUIC), `2`(WebSocket), `3`(HTTP/2) | `0` | Server only | | `dial` | Source IP for outbound | IP address or `auto` | `auto` | Both | | `read` | Read timeout duration | Time duration (e.g., `10m`, `30s`, `1h`) | `0` | Both | | `rate` | Bandwidth rate limit | Integer (Mbps), 0=unlimited | `0` | Both | | `slot` | Connection slot count | Integer (1-65536) | `65536` | Both | | `proxy` | PROXY protocol support | `0`(disabled), `1`(enabled) | `0` | Both | +| `block` | Protocol blocking | `0`(disabled), `1`(SOCKS), `2`(HTTP), `3`(TLS) | `0` | Both | | `notcp` | TCP support control | `0`(enabled), `1`(disabled) | `0` | Both | | `noudp` | UDP support control | `0`(enabled), `1`(disabled) | `0` | Both | \ No newline at end of file diff --git a/nodepass/docs/en/configuration.md b/nodepass/docs/en/configuration.md index 628a766b49..7891e5929c 100644 --- a/nodepass/docs/en/configuration.md +++ b/nodepass/docs/en/configuration.md @@ -213,59 +213,139 @@ nodepass "server://0.0.0.0:10101/remote.example.com:8080?log=info&tls=1&dial=10. - Binding failures trigger automatic fallback to prevent connection failures - This parameter does not affect incoming tunnel connections or server listen addresses -## QUIC Transport Protocol +## Connection Pool Types -NodePass supports QUIC as an alternative transport protocol for connection pooling in dual-end handshake mode. QUIC provides UDP-based multiplexed streams with built-in encryption and improved performance characteristics compared to traditional TCP pools. +NodePass supports three connection pool types for tunnel connection management in dual-end handshake mode. Each type provides different transport protocols and performance characteristics. -- `quic`: QUIC transport mode (default: 0) +- `type`: Connection pool type (default: 0) - Value 0: Use TCP-based connection pool (traditional pool library) - Value 1: Use QUIC-based connection pool (UDP multiplexing with streams) + - Value 2: Use WebSocket/WSS-based connection pool (HTTP upgrade connections) + - Value 3: Use HTTP/2-based connection pool (multiplexed streams over single TLS connection) - Only applies to dual-end handshake mode (mode=2) - Automatically enables TLS if not already configured (minimum tls=1) - - Uses QUIC streams for multiplexed connections over a single UDP connection - Server configuration is automatically delivered to client during handshake -**QUIC Advantages:** +### TCP Pool (type=0) + +Traditional TCP-based connection pool providing maximum compatibility and reliability. + +**Advantages:** +- Maximum network compatibility, TCP supported by virtually all networks +- Strict TCP semantics and ordering guarantees +- Stable performance in low-loss networks +- Widely accepted in enterprise environments +- Low latency in stable networks + +**Use Cases:** +- Networks that block or throttle UDP traffic +- Applications requiring strict TCP semantics +- Default choice in enterprise environments +- Maximum compatibility requirements + +### QUIC Pool (type=1) + +Modern connection pool based on QUIC protocol, providing UDP-based multiplexed streams. + +**Advantages:** - **Multiplexing**: Multiple streams over a single UDP connection - **Reduced Latency**: Faster connection establishment with 0-RTT support - **Better Loss Recovery**: Stream-level flow control and congestion management - **NAT Traversal**: UDP-based protocol works better through NATs and firewalls - **Built-in Encryption**: Mandatory TLS 1.3 encryption for all QUIC connections -**QUIC Requirements:** -- Only server needs to configure the `quic` parameter - client receives configuration automatically -- TLS mode must be enabled (tls=1 or tls=2) - automatically set if quic=1 -- Only available in dual-end handshake mode (mode=2 or mode=0 with remote addresses) -- Not applicable to single-end forwarding mode (mode=1) +**Use Cases:** +- Mobile networks or frequently changing network conditions +- High-latency connections (satellite, long-distance) +- NAT-heavy environments +- Real-time applications benefiting from stream independence +- Scenarios where 0-RTT reconnection provides value + +**Requirements:** +- TLS mode must be enabled (tls=1 or tls=2) +- Only available in dual-end handshake mode (mode=2) +- UDP port accessibility required + +### WebSocket Pool (type=2) + +Connection pool based on WebSocket protocol, establishing connections via HTTP upgrade. + +**Advantages:** +- **Proxy Traversal**: Can traverse HTTP proxies and CDNs +- **Firewall Friendly**: Uses standard HTTP/HTTPS ports, easily passes through firewalls +- **Web Infrastructure Compatible**: Integrates with existing web infrastructure +- **Bidirectional Communication**: Supports full-duplex communication +- **Wide Support**: Supported by all modern browsers and platforms + +**Use Cases:** +- Need to traverse HTTP proxies or CDNs +- Corporate environments allowing only HTTP/HTTPS traffic +- Firewalls blocking raw TCP connections +- Need compatibility with existing web infrastructure +- Web proxy or VPN alternative solutions + +**Requirements:** +- **TLS must be enabled** - WebSocket pool requires WSS (encrypted). Minimum `tls=1` required, `tls=2` recommended for production +- Only available in dual-end handshake mode (mode=2) +- TCP port with WebSocket upgrade support required +- **Important**: Type 2 does NOT support unencrypted mode (tls=0). If tls=0 is specified with type=2, system will automatically enforce tls=1 + +### HTTP/2 Pool (type=3) + +Connection pool based on HTTP/2 protocol, providing multiplexed streams over a single TLS connection. + +**Advantages:** +- **Stream Multiplexing**: Multiple independent streams over a single TCP connection +- **Header Compression**: HPACK compression reduces bandwidth usage +- **Binary Protocol**: Efficient binary framing reduces parsing overhead +- **Flow Control**: Per-stream and connection-level flow control +- **Server Push**: Potential for optimized data transfer patterns +- **TLS Integration**: Native TLS 1.3 support with strong encryption +- **Firewall Friendly**: Uses standard HTTPS ports and protocol patterns + +**Use Cases:** +- Corporate environments with HTTP/HTTPS-only policies +- Networks requiring protocol-level optimization and efficiency +- High-concurrency scenarios benefiting from stream multiplexing +- Environments needing both proxy traversal and performance +- Applications requiring fine-grained flow control +- Infrastructure with HTTP/2-aware load balancers or proxies + +**Requirements:** +- TLS mode must be enabled (tls=1 or tls=2) +- Only available in dual-end handshake mode (mode=2) +- HTTP/2 protocol support required (built into NodePass) + +### Configuration Examples -Example: ```bash -# Server with QUIC transport (automatically enables TLS) -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=1&mode=2" +# TCP pool (default) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=0&mode=2&tls=1" -# Client automatically adopts QUIC transport from server +# QUIC pool (automatically enables TLS) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=1&mode=2" + +# WebSocket pool (with custom TLS certificate) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=2&tls=2&crt=/path/to/cert.pem&key=/path/to/key.pem" + +# HTTP/2 pool (multiplexed streams with TLS) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=3&mode=2&tls=1" + +# Client automatically adopts server's pool type configuration nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2" - -# QUIC with custom TLS certificate (server-side only) -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=1&tls=2&crt=/path/to/cert.pem&key=/path/to/key.pem" - -# Traditional TCP pool (default behavior) -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=0&mode=2" ``` -**QUIC Use Cases:** -- **High-Latency Networks**: Reduced connection overhead in satellite or long-distance links -- **Mobile Networks**: Better handling of network transitions and packet loss -- **Real-Time Applications**: Lower latency for gaming, VoIP, or video streaming -- **NAT-Heavy Environments**: Improved connectivity through complex NAT scenarios -- **Concurrent Streams**: Efficient handling of multiple parallel data flows - **Important Notes:** -- QUIC mode requires UDP port accessibility on both server and client -- Firewall rules must allow UDP traffic on the tunnel port -- Some network middleboxes may block or deprioritize UDP traffic -- QUIC connection uses keep-alive and automatic reconnection -- Stream multiplexing shares bandwidth across all concurrent connections +- Only server needs to configure `type` parameter - client receives configuration automatically +- **WebSocket pool (type=2) requires TLS**: Minimum `tls=1`. If type=2 without TLS, system automatically sets tls=1 +- All pool types only available in dual-end handshake mode (mode=2 or mode=0 with remote addresses) +- Not applicable to single-end forwarding mode (mode=1) + +**Pool Type Use Cases:** +- **TCP Pool**: Standard enterprise environments, maximum compatibility, stable networks +- **QUIC Pool**: High-latency networks, mobile networks, real-time applications, complex NAT environments +- **WebSocket Pool**: HTTP proxy traversal, enterprise firewall restrictions, web infrastructure integration +- **HTTP/2 Pool**: HTTP/HTTPS-only policies, high-concurrency scenarios, protocol-level optimization needs ## Connection Pool Capacity Parameters @@ -278,15 +358,15 @@ Connection pool capacity parameters only apply to dual-end handshake mode and ar - The `max` parameter set by client will be overridden by the value delivered from server during handshake - The `min` parameter is fully controlled by client and will not be modified by server - In client single-end forwarding mode, connection pools are not used and these parameters are ignored -- Applies to both TCP pools (quic=0) and QUIC pools (quic=1) +- Applies to all connection pool types (type=0 for TCP, type=1 for QUIC, type=2 for WebSocket) Example: ```bash # Client sets minimum pool to 32, maximum pool will be determined by server nodepass "client://server.example.com:10101/127.0.0.1:8080?min=32" -# Client with QUIC and custom pool capacity -nodepass "client://server.example.com:10101/127.0.0.1:8080?quic=1&min=128" +# Client with QUIC pool and custom pool capacity +nodepass "client://server.example.com:10101/127.0.0.1:8080?type=1&min=128" ``` ## Data Read Timeout @@ -500,6 +580,100 @@ nodepass "server://0.0.0.0:10101/0.0.0.0:8080?log=info&tls=1&noudp=1" - Existing UDP sessions will be terminated when switching to noudp=1 - UDP buffer pools and session management are disabled when noudp=1 +## Protocol Blocking + +NodePass provides fine-grained protocol blocking capabilities to prevent specific protocols from being tunneled. This is useful for security policies that require blocking certain protocols while allowing others. + +The `block` parameter uses a numeric string where each digit represents a protocol category: +- `1`: Block SOCKS protocols (SOCKS4/4a/5) +- `2`: Block HTTP protocols (all HTTP methods) +- `3`: Block TLS/SSL protocols (encrypted connections) + +Multiple protocols can be blocked by including the corresponding digits in any order. The parameter value can contain duplicate digits without affecting behavior. + +### Configuration Options + +- `block`: Protocol blocking control (default: not set or `0`) + - Not set or `0`: Allow all protocols (no blocking) + - Contains `1`: Block SOCKS4, SOCKS4a, and SOCKS5 protocols + - Contains `2`: Block HTTP protocols (GET, POST, CONNECT, etc.) + - Contains `3`: Block TLS/SSL handshake (0x16 content type) + +### Examples + +Block SOCKS protocols only: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=1" +``` + +Block HTTP protocols only: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=2" +``` + +Block both SOCKS and HTTP: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=12" +# OR +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=21" +``` + +Block all three protocol categories: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=123" +# OR any combination like: 321, 213, 312, etc. +``` + +Combined with other security settings: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?log=info&tls=1&block=12&slot=1024" +``` + +### Detection Mechanism + +NodePass uses efficient protocol detection with minimal overhead: + +- **SOCKS Detection**: Examines the first 2 bytes of incoming connections + - SOCKS4/4a: Checks for version byte `0x04` and command byte `0x01`/`0x02` + - SOCKS5: Checks for version byte `0x05` and valid method count + +- **HTTP Detection**: Scans up to 8 bytes for HTTP method patterns + - Identifies uppercase letters followed by a space character + - Detects all standard HTTP methods (GET, POST, CONNECT, DELETE, etc.) + - Also detects custom HTTP methods and WebDAV extensions + +- **TLS Detection**: Examines the first byte for TLS handshake + - Identifies TLS handshake record type `0x16` + - Blocks TLS 1.0, 1.1, 1.2, and 1.3 handshakes + +### Use Cases + +**Block proxy protocols in tunnel services:** +```bash +# Allow only application traffic, block proxy protocols +nodepass "server://0.0.0.0:10101/app.backend.local:8080?block=12" +``` + +**Enforce encryption policy:** +```bash +# Allow only encrypted traffic, block plaintext protocols +nodepass "server://0.0.0.0:10101/0.0.0.0:443?block=12&tls=2" +``` + +**Prevent TLS-in-TLS:** +```bash +# Block nested TLS when outer layer already encrypts +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?tls=1&block=3" +``` + +### Important Notes + +- Protocol detection occurs at connection establishment (first bytes received) +- Blocked connections are immediately closed with a warning log entry +- This feature adds minimal CPU overhead (typically <0.1ms per connection) +- Protocol blocking applies to both single-end and dual-end forwarding modes +- Combine with `notcp`/`noudp` for complete traffic control + ## Target Address Groups and Load Balancing NodePass supports configuring multiple target addresses to achieve high availability and load balancing. Target address groups are only applicable to the egress side (the final destination of traffic) and should not be used on the ingress side. @@ -561,24 +735,26 @@ nodepass "client://127.0.0.1:3306/db-primary.local:3306,db-secondary.local:3306? NodePass allows flexible configuration via URL query parameters. The following table shows which parameters are applicable in server, client, and master modes: -| Parameter | Description | Default | server | client | master | -|-----------|--------------------------|-------------------|:------:|:------:|:------:| -| `log` | Log level | `info` | O | O | O | -| `tls` | TLS encryption mode | `0` | O | X | O | -| `crt` | Custom certificate path | N/A | O | X | O | -| `key` | Custom key path | N/A | O | X | O | -| `dns` | DNS cache TTL | `5m` | O | O | X | -| `min` | Minimum pool capacity | `64` | X | O | X | -| `max` | Maximum pool capacity | `1024` | O | X | X | -| `mode` | Run mode control | `0` | O | O | X | -| `quic` | QUIC protocol support | `0` | O | X | X | -| `dial` | Source IP for outbound | `auto` | O | O | X | -| `read` | Data read timeout | `0` | O | O | X | -| `rate` | Bandwidth rate limit | `0` | O | O | X | -| `slot` | Maximum connection limit | `65536` | O | O | X | -| `proxy` | PROXY protocol support | `0` | O | O | X | -| `notcp` | TCP support control | `0` | O | O | X | -| `noudp` | UDP support control | `0` | O | O | X | +| Parameter | Description | Default | Accepted Values | server | client | master | +|-----------|-------------|---------|-----------------|:------:|:------:|:------:| +| `log` | Log level | `info` | `none`/`debug`/`info`/`warn`/`error`/`event` | O | O | O | +| `tls` | TLS encryption mode | `0` | `0`/`1`/`2` | O | X | O | +| `crt` | Custom certificate path | N/A | File path | O | X | O | +| `key` | Custom key path | N/A | File path | O | X | O | +| `dns` | DNS cache TTL | `5m` | `30s`/`5m`/`1h` etc. | O | O | X | +| `sni` | Server Name Indication | `none` | Hostname | X | O | X | +| `min` | Minimum pool capacity | `64` | Positive integer | X | O | X | +| `max` | Maximum pool capacity | `1024` | Positive integer | O | X | X | +| `mode` | Run mode control | `0` | `0`/`1`/`2` | O | O | X | +| `type` | Connection pool type | `0` | `0`/`1`/`2`/`3` | O | X | X | +| `dial` | Source IP for outbound | `auto` | `auto`/IP address | O | O | X | +| `read` | Data read timeout | `0` | `0`/`30s`/`5m` etc. | O | O | X | +| `rate` | Bandwidth rate limit | `0` | `0` or integer (Mbps) | O | O | X | +| `slot` | Maximum connection limit | `65536` | `0` or integer | O | O | X | +| `proxy` | PROXY protocol support | `0` | `0`/`1` | O | O | X | +| `block` | Protocol blocking | `0` | `0`/`1`/`2`/`3` | O | O | X | +| `notcp` | TCP support control | `0` | `0`/`1` | O | O | X | +| `noudp` | UDP support control | `0` | `0`/`1` | O | O | X | - O: Parameter is valid and recommended for configuration - X: Parameter is not applicable and should be ignored @@ -588,9 +764,10 @@ NodePass allows flexible configuration via URL query parameters. The following t - For client/server dual-end handshake modes, adjust connection pool capacity (`min`, `max`) based on traffic and resource constraints for optimal performance. - Use run mode control (`mode`) when automatic detection doesn't match your deployment requirements or for consistent behavior across environments. - Configure rate limiting (`rate`) to control bandwidth usage and prevent network congestion in shared environments. -- Configure QUIC transport (`quic`) on the server only - clients automatically receive the configuration during handshake. +- Configure connection pool type (`type`) on the server only - clients automatically receive the configuration during handshake. - Set `notcp=1` when only UDP traffic needs to be tunneled to reduce resource usage and simplify configuration. - Set `noudp=1` when only TCP traffic needs to be tunneled to reduce resource usage and simplify configuration. +- Use `block` parameter to enforce security policies by blocking specific protocol categories (SOCKS/HTTP/TLS). - Log level (`log`) can be set in all modes for easier operations and troubleshooting. ## Environment Variables diff --git a/nodepass/docs/en/examples.md b/nodepass/docs/en/examples.md index e06a65dad0..ea96998b74 100644 --- a/nodepass/docs/en/examples.md +++ b/nodepass/docs/en/examples.md @@ -137,9 +137,85 @@ This setup: - Limits logging to warnings and errors only - Maps service A's API to appear as a local service on service B +## Protocol Blocking and Traffic Filtering + +### Example 9: Block Proxy Protocols + +Prevent SOCKS and HTTP proxy usage through your tunnel: + +```bash +# Server that blocks both SOCKS and HTTP proxy protocols +nodepass "server://0.0.0.0:10101/app.backend.com:8080?block=12&tls=1" + +# Client connecting to the protected server +nodepass "client://server.example.com:10101/127.0.0.1:8080" +``` + +This configuration: +- Blocks all SOCKS4/4a/5 proxy connections (`block` contains `1`) +- Blocks all HTTP proxy methods like CONNECT, GET, POST (`block` contains `2`) +- Allows only application-specific protocols through the tunnel +- Useful for preventing proxy abuse on application tunnels + +### Example 10: Block TLS-in-TLS Scenarios + +Prevent nested TLS encryption when outer layer already provides security: + +```bash +# Server with TLS encryption that blocks inner TLS connections +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?tls=1&block=3" + +# Client automatically inherits TLS settings +nodepass "client://server.example.com:10101/127.0.0.1:8080" +``` + +This setup: +- Encrypts the tunnel itself with TLS (`tls=1`) +- Blocks TLS handshakes inside the encrypted tunnel (`block=3`) +- Prevents unnecessary double encryption overhead +- Helps identify misconfigurations where applications try to add redundant TLS + +### Example 11: Comprehensive Security Policy + +Enforce strict security policy allowing only application traffic: + +```bash +# Production server with comprehensive protocol blocking +nodepass "server://0.0.0.0:10101/secure-app.internal:443?tls=2&crt=/path/to/cert.pem&key=/path/to/key.pem&block=123&slot=500" + +# Client with enforced encryption +nodepass "client://prod-server.example.com:10101/127.0.0.1:8443?log=warn" +``` + +This configuration: +- Uses verified custom certificates for maximum security (`tls=2`) +- Blocks SOCKS proxies (`block` contains `1`) +- Blocks HTTP proxies (`block` contains `2`) +- Blocks nested TLS connections (`block` contains `3`) +- Limits concurrent connections to 500 for resource control +- Only logs warnings and errors to reduce noise + +### Example 12: Selective Protocol Blocking for Development + +Allow HTTP traffic while blocking proxies in development environment: + +```bash +# Development server that blocks only SOCKS protocols +nodepass "server://127.0.0.1:10101/localhost:3000?block=1&log=debug" + +# Development client +nodepass "client://127.0.0.1:10101/localhost:8080" +``` + +This setup: +- Blocks SOCKS protocols but allows HTTP requests +- Useful for testing web applications that need HTTP methods +- Prevents developers from tunneling SOCKS proxy traffic +- Enables debug logging for troubleshooting + ## Bandwidth Rate Limiting -### Example 9: File Transfer Server with Rate Limit +### Example 13: File Transfer Server with Rate Limit Control bandwidth usage for file transfer services: @@ -157,7 +233,7 @@ This configuration: - Allows file transfers while preserving bandwidth for other services - Uses TLS encryption for secure file transfer -### Example 10: IoT Sensor Data Collection with Conservative Limits +### Example 14: IoT Sensor Data Collection with Conservative Limits For IoT devices with limited bandwidth or metered connections: @@ -175,7 +251,7 @@ This setup: - Minimal logging (warn/error) to reduce resource usage on IoT devices - Efficient for MQTT or other IoT protocols -### Example 11: Development Environment Rate Control +### Example 15: Development Environment Rate Control Testing applications under bandwidth constraints: @@ -194,7 +270,7 @@ This configuration: ## IoT Device Management -### Example 12: IoT Gateway +### Example 16: IoT Gateway Create a central access point for IoT devices: @@ -215,7 +291,7 @@ This configuration: ## Multi-Homed Systems and Source IP Control -### Example 13: Specific Network Interface Selection +### Example 17: Specific Network Interface Selection Control which network interface is used for outbound connections on multi-homed systems: @@ -234,7 +310,7 @@ This configuration: - Automatically falls back to system-selected IP if specified address fails - Supports both IPv4 and IPv6 addresses -### Example 14: Network Segmentation and VLAN Routing +### Example 18: Network Segmentation and VLAN Routing Direct traffic through specific network segments or VLANs: @@ -265,7 +341,7 @@ This setup: ## DNS Cache TTL Configuration -### Example 15: Stable Corporate Network +### Example 19: Stable Corporate Network Use longer TTL for stable internal services: @@ -283,7 +359,7 @@ This configuration: - Improves connection performance by minimizing DNS lookups - Suitable for production environments with stable DNS -### Example 16: Dynamic DNS Environments +### Example 20: Dynamic DNS Environments Use shorter TTL for frequently changing DNS records: @@ -301,7 +377,7 @@ This setup: - Ensures connections use current DNS records - Ideal for cloud environments with frequent IP changes -### Example 17: Development and Testing +### Example 21: Development and Testing Disable caching for development environments: @@ -319,7 +395,7 @@ This configuration: - Useful during development when DNS records change frequently - Helps identify DNS-related issues during testing -### Example 18: Mixed Environment with Custom TTL +### Example 22: Mixed Environment with Custom TTL Balance performance and freshness with moderate TTL: @@ -349,7 +425,7 @@ This setup: ## High Availability and Load Balancing -### Example 19: Multi-Backend Server Load Balancing +### Example 23: Multi-Backend Server Load Balancing Use target address groups for even traffic distribution and automatic failover: @@ -367,7 +443,7 @@ This configuration: - Automatically resumes sending traffic to recovered servers - Uses TLS encryption to secure the tunnel -### Example 20: Database Primary-Replica Failover +### Example 24: Database Primary-Replica Failover Configure primary and replica database instances for high availability access: @@ -382,7 +458,7 @@ This setup: - Application requires no modification for transparent failover - Logs only warnings and errors to reduce output -### Example 21: API Gateway Backend Pool +### Example 25: API Gateway Backend Pool Configure multiple backend service instances for an API gateway: @@ -400,7 +476,7 @@ This configuration: - Client limits bandwidth to 100 Mbps with maximum 2000 concurrent connections - Single instance failure doesn't affect overall service availability -### Example 22: Geo-Distributed Services +### Example 26: Geo-Distributed Services Configure multi-region service nodes to optimize network latency: @@ -424,7 +500,7 @@ This setup: ## PROXY Protocol Integration -### Example 23: Load Balancer Integration with PROXY Protocol +### Example 27: Load Balancer Integration with PROXY Protocol Enable PROXY protocol support for integration with load balancers and reverse proxies: @@ -443,7 +519,7 @@ This configuration: - Compatible with HAProxy, Nginx, and other PROXY protocol aware services - Useful for maintaining accurate access logs and IP-based access controls -### Example 24: Reverse Proxy Support for Web Applications +### Example 28: Reverse Proxy Support for Web Applications Enable web applications behind NodePass to receive original client information: @@ -467,7 +543,7 @@ This setup: - Supports compliance requirements for connection auditing - Works with web servers that support PROXY protocol (Nginx, HAProxy, etc.) -### Example 25: Database Access with Client IP Preservation +### Example 29: Database Access with Client IP Preservation Maintain client IP information for database access logging and security: @@ -494,7 +570,7 @@ Benefits: ## Container Deployment -### Example 26: Containerized NodePass +### Example 30: Containerized NodePass Deploy NodePass in a Docker environment: @@ -529,7 +605,7 @@ This configuration: ## Master API Management -### Example 27: Centralized Management +### Example 31: Centralized Management Set up a central controller for multiple NodePass instances: @@ -566,7 +642,7 @@ This setup: - Offers a RESTful API for automation and integration - Includes a built-in Swagger UI at http://localhost:9090/api/v1/docs -### Example 28: Custom API Prefix +### Example 32: Custom API Prefix Use a custom API prefix for the master mode: @@ -585,7 +661,7 @@ This allows: - Custom URL paths for security or organizational purposes - Swagger UI access at http://localhost:9090/admin/v1/docs -### Example 29: Real-time Connection and Traffic Monitoring +### Example 33: Real-time Connection and Traffic Monitoring Monitor instance connection counts and traffic statistics through the master API: @@ -623,17 +699,17 @@ This monitoring setup provides: - **Capacity planning**: Resource planning based on historical connection data - **Troubleshooting**: Abnormal connection count changes may indicate network issues -## QUIC Transport Protocol +## Connection Pool Types -### Example 30: QUIC-based Tunnel with Stream Multiplexing +### Example 34: QUIC-based Tunnel with Stream Multiplexing Use QUIC protocol for connection pooling with improved performance in high-latency networks: ```bash -# Server side: Enable QUIC transport -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=1&mode=2&tls=1&log=debug" +# Server side: Enable QUIC pool +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=1&mode=2&tls=1&log=debug" -# Client side: Automatically receives QUIC configuration from server +# Client side: Automatically receives pool type configuration from server nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=debug" ``` @@ -643,17 +719,17 @@ This configuration: - Mandatory TLS 1.3 encryption (automatically enabled) - Better performance in packet loss scenarios (no head-of-line blocking) - Improved connection establishment with 0-RTT support -- Client automatically receives QUIC configuration from server during handshake +- Client automatically receives pool type configuration from server during handshake -### Example 31: QUIC with Custom TLS Certificate +### Example 35: QUIC Pool with Custom TLS Certificate Deploy QUIC tunnel with verified certificates for production: ```bash -# Server side: QUIC with custom certificate -nodepass "server://0.0.0.0:10101/backend.internal:8080?quic=1&mode=2&tls=2&crt=/etc/nodepass/cert.pem&key=/etc/nodepass/key.pem" +# Server side: QUIC pool with custom certificate +nodepass "server://0.0.0.0:10101/backend.internal:8080?type=1&mode=2&tls=2&crt=/etc/nodepass/cert.pem&key=/etc/nodepass/key.pem" -# Client side: Automatically receives QUIC configuration with certificate verification +# Client side: Automatically receives pool type configuration with certificate verification nodepass "client://tunnel.example.com:10101/127.0.0.1:8080?mode=2&min=64&log=info" ``` @@ -662,17 +738,62 @@ This setup: - QUIC protocol provides mandatory TLS 1.3 encryption - Suitable for production environments - Full certificate validation on client side -- QUIC configuration automatically delivered from server +- Pool type configuration automatically delivered from server -### Example 32: QUIC for Mobile/High-Latency Networks +### Example 36: WebSocket Pool for Proxy Traversal + +Use WebSocket pool behind enterprise firewalls: + +```bash +# Server side: Enable WebSocket pool (TLS required) +nodepass "server://0.0.0.0:10101/internal.backend:8080?type=2&mode=2&tls=1&log=info" + +# Client side: Automatically receives WebSocket configuration +nodepass "client://wss.tunnel.com:10101/127.0.0.1:8080?mode=2&min=64" +``` + +This configuration: +- Uses WebSocket protocol to traverse HTTP proxies and CDNs +- **Requires TLS encryption** - minimum `tls=1`, use `tls=2` with certificates for production +- Uses standard HTTPS ports, easily passes through firewalls +- Compatible with existing web infrastructure +- Supports full-duplex communication +- Suitable for enterprise environments allowing only HTTP/HTTPS traffic +- Client automatically adopts server's pool type configuration +- **Note**: WebSocket pool does not support unencrypted mode (tls=0) + +### Example 37: HTTP/2 Pool for High-Concurrency Environments + +Use HTTP/2 pool for efficient multiplexed streams with protocol optimization: + +```bash +# Server side: Enable HTTP/2 pool (TLS required) +nodepass "server://0.0.0.0:10101/backend.internal:8080?type=3&mode=2&tls=1&log=info" + +# Client side: Automatically receives HTTP/2 configuration +nodepass "client://h2.tunnel.com:10101/127.0.0.1:8080?mode=2&min=64" +``` + +This configuration: +- Uses HTTP/2 protocol for multiplexed streams over a single TLS connection +- **Requires TLS encryption** - minimum `tls=1`, use `tls=2` with certificates for production +- HPACK header compression reduces bandwidth usage +- Binary framing protocol with efficient parsing +- Per-stream flow control for optimal resource utilization +- Works with HTTP/2-aware proxies and load balancers +- Suitable for HTTP/HTTPS-only policy environments +- Client automatically adopts server's pool type configuration +- Ideal for high-concurrency scenarios benefiting from stream multiplexing + +### Example 38: QUIC Pool for Mobile/High-Latency Networks Optimize for mobile networks or satellite connections: ```bash -# Server side: QUIC with adaptive pool sizing -nodepass "server://0.0.0.0:10101/api.backend:443?quic=1&mode=2&max=512&tls=1&log=info" +# Server side: QUIC pool with adaptive pool sizing +nodepass "server://0.0.0.0:10101/api.backend:443?type=1&mode=2&max=512&tls=1&log=info" -# Client side: Automatically receives QUIC, configure larger minimum pool for mobile +# Client side: Automatically receives pool type, configure larger minimum pool for mobile nodepass "client://mobile.tunnel.com:10101/127.0.0.1:8080?mode=2&min=256&log=warn" ``` @@ -682,20 +803,28 @@ This configuration: - Stream multiplexing reduces connection overhead - Better handling of packet loss and jitter - 0-RTT reconnection for faster recovery after network changes -- Client automatically adopts QUIC from server +- Client automatically adopts pool type from server -### Example 33: QUIC vs TCP Pool Performance Comparison +### Example 39: Pool Type Performance Comparison -Side-by-side comparison of QUIC and TCP pools: +Side-by-side comparison of TCP, QUIC, WebSocket, and HTTP/2 pools: ```bash # Traditional TCP pool (default) -nodepass "server://0.0.0.0:10101/backend.example.com:8080?quic=0&mode=2&tls=1&log=event" +nodepass "server://0.0.0.0:10101/backend.example.com:8080?type=0&mode=2&tls=1&log=event" nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=event" # QUIC pool (modern approach) -nodepass "server://0.0.0.0:10102/backend.example.com:8080?quic=1&mode=2&tls=1&log=event" +nodepass "server://0.0.0.0:10102/backend.example.com:8080?type=1&mode=2&tls=1&log=event" nodepass "client://server.example.com:10102/127.0.0.1:8081?mode=2&min=128&log=event" + +# WebSocket pool (proxy traversal) +nodepass "server://0.0.0.0:10103/backend.example.com:8080?type=2&mode=2&tls=1&log=event" +nodepass "client://server.example.com:10103/127.0.0.1:8082?mode=2&min=128&log=event" + +# HTTP/2 pool (multiplexed streams) +nodepass "server://0.0.0.0:10104/backend.example.com:8080?type=3&mode=2&tls=1&log=event" +nodepass "client://server.example.com:10104/127.0.0.1:8083?mode=2&min=128&log=event" ``` **TCP Pool Advantages**: @@ -710,15 +839,29 @@ nodepass "client://server.example.com:10102/127.0.0.1:8081?mode=2&min=128&log=ev - Improved NAT traversal capabilities - Single UDP socket reduces resource usage -### Example 34: QUIC for Real-Time Applications +**WebSocket Pool Advantages**: +- Can traverse HTTP proxies and CDNs +- Uses standard HTTP/HTTPS ports +- Integrates with existing web infrastructure +- Suitable for enterprise firewall environments + +**HTTP/2 Pool Advantages**: +- Efficient stream multiplexing over single TCP connection +- HPACK header compression reduces bandwidth +- Binary protocol with efficient parsing +- Per-stream flow control for resource optimization +- Works with HTTP/2-aware infrastructure +- Ideal for HTTP/HTTPS-only policy environments + +### Example 40: QUIC Pool for Real-Time Applications Configure QUIC tunnel for gaming, VoIP, or video streaming: ```bash -# Server side: QUIC with optimized settings for real-time traffic -nodepass "server://0.0.0.0:10101/gameserver.local:7777?quic=1&mode=2&tls=1&read=30s&slot=10000" +# Server side: QUIC pool with optimized settings for real-time traffic +nodepass "server://0.0.0.0:10101/gameserver.local:7777?type=1&mode=2&tls=1&read=30s&slot=10000" -# Client side: Automatically receives QUIC configuration from server +# Client side: Automatically receives pool type configuration from server nodepass "client://game.tunnel.com:10101/127.0.0.1:7777?mode=2&min=64&read=30s" ``` @@ -728,14 +871,13 @@ This setup: - 30-second read timeout for quick detection of stale connections - Large slot limit supports many concurrent players/streams - Reduced connection establishment overhead -- Client automatically adopts server's QUIC configuration +- Client automatically adopts server's pool type configuration -**QUIC Use Case Summary**: -- **Mobile Networks**: Better handling of network transitions and packet loss -- **High-Latency Links**: Reduced overhead from 0-RTT and multiplexing -- **Real-Time Apps**: Stream independence prevents head-of-line blocking -- **NAT-Heavy Environments**: UDP-based protocol traverses NATs more reliably -- **Concurrent Streams**: Efficient bandwidth sharing across parallel flows +**Connection Pool Type Use Case Summary**: +- **TCP Pool**: Standard enterprise environments, maximum compatibility, stable networks +- **QUIC Pool**: Mobile networks, high-latency links, real-time apps, complex NAT environments +- **WebSocket Pool**: HTTP proxy traversal, enterprise firewall restrictions, web infrastructure integration +- **HTTP/2 Pool**: High-concurrency HTTP/HTTPS services, bandwidth optimization, HTTP/2-aware environments ## Next Steps diff --git a/nodepass/docs/en/how-it-works.md b/nodepass/docs/en/how-it-works.md index 3a2ee59ddd..dd579fd283 100644 --- a/nodepass/docs/en/how-it-works.md +++ b/nodepass/docs/en/how-it-works.md @@ -210,27 +210,33 @@ NodePass uses a sophisticated URL-based signaling protocol through the TCP tunne ## Connection Pool Architecture -NodePass implements an efficient connection pooling system for managing network connections, which forms the core of its performance advantages. NodePass supports two transport protocols for connection pools: traditional TCP-based pools and modern QUIC-based UDP pools. +NodePass implements an efficient connection pooling system for managing network connections, which forms the core of its performance advantages. NodePass supports three transport protocols for connection pools: traditional TCP-based pools, modern QUIC-based UDP pools, and WebSocket-based pools. ### Transport Protocol Selection -NodePass provides two connection pool transport options via the `quic` parameter: +NodePass provides three connection pool transport options via the `type` parameter: -1. **TCP-based Pool (quic=0, default)**: +1. **TCP-based Pool (type=0, default)**: - Traditional TCP connections managed by the `pool` library - Multiple independent TCP connections between client and server - Standard TLS encryption over individual TCP connections - Well-tested and widely compatible approach -2. **QUIC-based Pool (quic=1)**: +2. **QUIC-based Pool (type=1)**: - UDP-based multiplexed streams managed by the `quic` library - Single QUIC connection with multiple concurrent streams - Mandatory TLS 1.3 encryption with 0-RTT support - Superior performance in high-latency and mobile networks +3. **WebSocket-based Pool (type=2)**: + - WebSocket connections established via HTTP upgrade + - Can traverse HTTP proxies and CDNs + - Uses standard HTTPS ports + - Suitable for enterprise environments and firewall-restricted scenarios + ### QUIC Pool Architecture -When `quic=1` is enabled, NodePass uses QUIC protocol for connection pooling with the following characteristics: +When `type=1` is enabled, NodePass uses QUIC protocol for connection pooling with the following characteristics: **Stream Multiplexing**: - Single UDP connection carries multiple bidirectional streams @@ -283,12 +289,69 @@ When `quic=1` is enabled, NodePass uses QUIC protocol for connection pooling wit - Improved NAT traversal compared to multiple TCP connections - Lower latency in packet loss scenarios (no head-of-line blocking) +### WebSocket Pool Architecture + +When `type=2` is enabled, NodePass uses WebSocket protocol for connection pooling with the following characteristics: + +**HTTP Upgrade Mechanism**: +- WebSocket connections established via standard HTTP upgrade requests +- Compatible with HTTP/1.1 proxies and CDNs +- Uses standard port 80 (ws) or 443 (wss) +- Supports custom HTTP headers for authentication and routing + +**Connection Establishment**: +- Server listens on TCP port for HTTP requests +- Client initiates HTTP upgrade request to WebSocket +- After handshake completion, connection upgrades to full-duplex WebSocket +- Server assigns unique connection ID for each incoming connection + +**Connection Lifecycle**: +1. **Connection Creation** (Server side): + - Server accepts HTTP upgrade request from authorized client + - Validates WebSocket handshake parameters (Origin, protocol, etc.) + - Upgrades connection and adds it to connection pool + - Generates connection ID sent to client for correlation + +2. **Connection Retrieval** (Client side): + - Client receives connection ID via control channel + - Client retrieves corresponding connection from WebSocket pool + - Connection wrapped as `net.Conn` for compatibility + - Connection used for data exchange with target endpoint + +3. **Connection Termination**: + - WebSocket close frame sent after data exchange completes + - Graceful closure with proper cleanup of underlying TCP connection + - Supports close reason codes and description messages + +**Dynamic Management**: +- Pool capacity adjusted based on connection creation success rate +- Connection creation intervals adapt to pool utilization +- Automatic connection capacity scaling within min/max boundaries +- Ping/Pong frames maintain connection health status + +**Security Features**: +- Supports WSS (WebSocket Secure) with TLS encryption +- TLS is required for WebSocket pool +- Two TLS modes supported: + - Mode 1: WSS with self-signed certificates + - Mode 2: WSS with full certificate verification for production +- Origin validation prevents cross-site WebSocket hijacking +- Supports custom authentication headers + +**Traversal Advantages**: +- Uses standard HTTP/HTTPS ports, easy to traverse firewalls +- Compatible with enterprise HTTP proxies and load balancers +- Can be deployed through CDNs and reverse proxies +- Blends with HTTP traffic, reducing detection and blocking risks +- Requires TLS encryption + ### Design Philosophy The connection pool design follows the principle of "warm-up over cold start," eliminating network latency through pre-established connections. This design philosophy draws from modern high-performance server best practices, amortizing the cost of connection establishment to the system startup phase rather than bearing this overhead on the critical path. -Both TCP and QUIC pools share this philosophy but implement it differently: -- **TCP pools**: Pre-establish multiple TCP connections +All three pool types share this philosophy but implement it differently: +- **TCP pools**: Pre-establish multiple independent TCP connections - **QUIC pools**: Pre-create multiple streams over a single QUIC connection +- **WebSocket pools**: Pre-establish multiple WebSocket connections ### Pool Design 1. **Pool Types**: @@ -302,7 +365,10 @@ Both TCP and QUIC pools share this philosophy but implement it differently: - Minimum capacity set by client, ensuring basic connection guarantee for client - Maximum capacity delivered by server during handshake, enabling global resource coordination - **Interval Control**: Time-based throttling between connection/stream creations, preventing network resource overload - - **Connection Factory**: Customizable connection creation function (TCP) or stream management (QUIC) + - **Connection Factory**: Customizable connection creation function + - TCP mode: Standard TCP dialing and TLS handshake + - QUIC mode: Stream management and multiplexing + - WebSocket mode: HTTP upgrade and handshake handling ### Advanced Design Features 1. **Zero-Latency Connections**: @@ -323,6 +389,7 @@ Both TCP and QUIC pools share this philosophy but implement it differently: - IDs and connections are stored in the pool with copy-on-write and delayed deletion strategies - **TCP Mode**: Creates individual TCP connections with optional TLS - **QUIC Mode**: Opens bidirectional streams over shared QUIC connection + - **WebSocket Mode**: Establishes WebSocket connections via HTTP upgrade 2. **Connection Acquisition**: - Client retrieves connections using connection IDs, supporting precise matching and fast lookups diff --git a/nodepass/docs/en/troubleshooting.md b/nodepass/docs/en/troubleshooting.md index 5d7f30ee7f..ab69d402bd 100644 --- a/nodepass/docs/en/troubleshooting.md +++ b/nodepass/docs/en/troubleshooting.md @@ -370,11 +370,11 @@ This guide helps you diagnose and resolve common issues you might encounter when - In production environments, recommend regularly backing up `nodepass.gob` to different storage locations - Use configuration management tools to save text-form backups of instance configurations -## QUIC-Specific Issues +## Connection Pool Type Issues -### QUIC Connection Failures +### QUIC Pool Connection Failures -**Symptoms**: QUIC tunnel fails to establish when `quic=1` is enabled. +**Symptoms**: QUIC pool tunnel fails to establish when `type=1` is enabled. **Possible Causes and Solutions**: @@ -386,14 +386,14 @@ This guide helps you diagnose and resolve common issues you might encounter when 2. **TLS Configuration Issues** - QUIC requires TLS to be enabled (minimum `tls=1`) - - If `quic=1` is set but TLS is disabled, system auto-enables `tls=1` + - If `type=1` is set but TLS is disabled, system auto-enables `tls=1` - For production, use `tls=2` with valid certificates - Check certificate validity for QUIC connections -3. **Client-Server QUIC Mismatch** - - Both server and client must use same `quic` setting - - Server with `quic=1` requires client with `quic=1` - - Server with `quic=0` requires client with `quic=0` +3. **Client-Server Pool Type Mismatch** + - Both server and client must use same `type` setting + - Server with `type=1` requires client with `type=1` + - Server with `type=0` requires client with `type=0` - Check logs for "QUIC connection not available" errors 4. **Mode Compatibility** @@ -401,16 +401,77 @@ This guide helps you diagnose and resolve common issues you might encounter when - Not available in single-end forwarding mode (mode=1) - System will fall back to TCP pool if mode incompatible -### QUIC Performance Issues +### WebSocket Pool Connection Failures -**Symptoms**: QUIC tunnel has lower performance than expected or worse than TCP pool. +**Symptoms**: WebSocket pool tunnel fails to establish when `type=2` is enabled. + +**Possible Causes and Solutions**: + +1. **HTTP/WebSocket Port Blocked** + - Verify TCP port is accessible with WebSocket protocol support + - Check firewall rules and proxy configurations + - Some proxies or CDNs may interfere with WebSocket upgrade + - Test connectivity with WebSocket client tools + +2. **TLS Configuration Issues** + - WebSocket Secure (WSS) requires TLS to be enabled (minimum `tls=1`) + - **WebSocket pool does NOT support unencrypted mode** - `tls=0` is not allowed for type=2 + - If `type=2` is set but TLS is disabled, system will automatically enforce `tls=1` + - For production, use `tls=2` with valid certificates + - Check certificate validity for WSS connections + +3. **Client-Server Pool Type Mismatch** + - Both server and client must use same `type` setting + - Server with `type=2` requires client with `type=2` + - Configuration is automatically delivered during handshake + - Check logs for "WebSocket connection not available" errors + +### HTTP/2 Pool Connection Failures + +**Symptoms**: HTTP/2 pool tunnel fails to establish when `type=3` is enabled. + +**Possible Causes and Solutions**: + +1. **TCP Port or HTTP/2 Protocol Blocked** + - Verify TCP port is accessible with HTTP/2 protocol support + - Check firewall rules and network policies + - Some networks may block or inspect HTTPS traffic + - Test connectivity with HTTP/2-capable client tools + +2. **TLS Configuration Issues** + - HTTP/2 requires TLS to be enabled (minimum `tls=1`) + - If `type=3` is set but TLS is disabled, system will automatically enforce `tls=1` + - For production, use `tls=2` with valid certificates + - HTTP/2 requires TLS 1.3 with ALPN (Application-Layer Protocol Negotiation) + - Check certificate validity and ALPN configuration + +3. **Client-Server Pool Type Mismatch** + - Both server and client must use same `type` setting + - Server with `type=3` requires client with `type=3` + - Configuration is automatically delivered during handshake + - Check logs for "HTTP/2 connection not available" errors + +4. **Mode Compatibility** + - HTTP/2 pool only works in dual-end handshake mode (mode=2) + - Not available in single-end forwarding mode (mode=1) + - System will fall back to TCP pool if mode incompatible + +5. **HTTP/2 Protocol Negotiation Failures** + - Verify ALPN extension is enabled and negotiates "h2" protocol + - Some older TLS implementations may not support ALPN + - Check logs for protocol negotiation errors + - Ensure both endpoints support HTTP/2 over TLS + +### QUIC Pool Performance Issues + +**Symptoms**: QUIC pool tunnel has lower performance than expected or worse than TCP pool. **Possible Causes and Solutions**: 1. **Network Path Issues** - Some networks deprioritize or shape UDP traffic - Check if network middleboxes are interfering with QUIC - - Consider testing with TCP pool (`quic=0`) for comparison + - Consider testing with TCP pool (`type=0`) for comparison - Monitor packet loss rates - QUIC performs better with low loss 2. **Pool Capacity Configuration** @@ -429,6 +490,29 @@ This guide helps you diagnose and resolve common issues you might encounter when - Test with both TCP and QUIC pools to compare performance - Consider TCP pool for applications requiring strict ordering +### WebSocket Pool Performance Issues + +**Symptoms**: WebSocket pool tunnel has lower performance than expected. + +**Possible Causes and Solutions**: + +1. **Proxy/CDN Overhead** + - WebSocket connections through proxies may add latency + - Check if intermediate proxies are buffering traffic + - Consider using TCP pool (`type=0`) or QUIC pool (`type=1`) for comparison + - Direct connections usually perform better than proxied + +2. **Frame Overhead** + - WebSocket protocol adds framing overhead to each message + - Larger message sizes reduce relative overhead + - Monitor frame sizes and adjust application behavior if needed + - Balance between latency and throughput + +3. **TLS Handshake Overhead** + - WSS requires TLS handshake for each connection + - Use connection pooling to amortize handshake costs + - Increase `min` and `max` parameters for better performance + ### QUIC Stream Exhaustion **Symptoms**: "Insufficient streams" errors or connection timeouts when using QUIC. @@ -453,16 +537,23 @@ This guide helps you diagnose and resolve common issues you might encounter when - NAT timeout may drop UDP connection - adjust NAT settings - Increase connection timeout if network latency is high -### QUIC vs TCP Pool Decision +### Connection Pool Type Decision -**When to Use QUIC** (`quic=1`): +**When to Use QUIC Pool** (`type=1`): - Mobile networks or frequently changing network conditions - High-latency connections (satellite, long-distance) - NAT-heavy environments where UDP traversal is better - Real-time applications benefiting from stream independence - Scenarios where 0-RTT reconnection provides value -**When to Use TCP Pool** (`quic=0`): +**When to Use WebSocket Pool** (`type=2`): +- Need to traverse HTTP proxies or CDNs +- Corporate environments allowing only HTTP/HTTPS traffic +- Environments where firewalls block raw TCP connections +- Need compatibility with existing web infrastructure +- Web proxy or VPN alternative solutions + +**When to Use TCP Pool** (`type=0`): - Networks that block or severely throttle UDP traffic - Applications requiring strict TCP semantics - Corporate environments with UDP restrictions @@ -472,12 +563,16 @@ This guide helps you diagnose and resolve common issues you might encounter when **Comparison Testing**: ```bash # Test TCP pool performance -nodepass "server://0.0.0.0:10101/backend:8080?quic=0&mode=2&log=event" -nodepass "client://server:10101/127.0.0.1:8080?quic=0&mode=2&log=event" +nodepass "server://0.0.0.0:10101/backend:8080?type=0&mode=2&log=event" +nodepass "client://server:10101/127.0.0.1:8080?mode=2&log=event" # Test QUIC pool performance -nodepass "server://0.0.0.0:10102/backend:8080?quic=1&mode=2&log=event" -nodepass "client://server:10102/127.0.0.1:8081?quic=1&mode=2&log=event" +nodepass "server://0.0.0.0:10102/backend:8080?type=1&mode=2&log=event" +nodepass "client://server:10102/127.0.0.1:8081?mode=2&log=event" + +# Test WebSocket pool performance +nodepass "server://0.0.0.0:10103/backend:8080?type=2&mode=2&log=event" +nodepass "client://server:10103/127.0.0.1:8082?mode=2&log=event" ``` Monitor traffic statistics and choose based on observed performance. diff --git a/nodepass/docs/en/usage.md b/nodepass/docs/en/usage.md index 0447ee8b70..e7dcbbc7e8 100644 --- a/nodepass/docs/en/usage.md +++ b/nodepass/docs/en/usage.md @@ -7,7 +7,7 @@ NodePass creates tunnels with an unencrypted TCP control channel and configurabl The general syntax for NodePass commands is: ```bash -nodepass ":///?log=&tls=&crt=&key=&dns=&min=&max=&mode=&quic=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" +nodepass ":///?log=&tls=&crt=&key=&dns=&min=&max=&mode=&type=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" ``` Where: @@ -23,7 +23,7 @@ Common query parameters: - `min=`: Minimum connection pool capacity (default: 64, set by client) - `max=`: Maximum connection pool capacity (default: 1024, set by server and delivered to client) - `mode=`: Run mode control (`0`, `1`, or `2`) - controls operational behavior -- `quic=`: QUIC transport mode (`0` for TCP pool, `1` for QUIC UDP pool, default: 0, server-side only) +- `type=`: Connection pool type (`0` for TCP pool, `1` for QUIC UDP pool, `2` for WebSocket/WSS pool, `3` for HTTP/2 pool, default: 0, server-side only) - `dial=`: Source IP address for outbound connections (default: `auto`, supports both IPv4 and IPv6) - `read=`: Data read timeout duration (default: 0, supports time units like 30s, 5m, 1h, etc.) - `rate=`: Bandwidth rate limit in Mbps (default: 0 for unlimited) @@ -37,10 +37,10 @@ TLS-related parameters (server/master modes only): - `crt=`: Path to certificate file (when `tls=2`) - `key=`: Path to private key file (when `tls=2`) -QUIC transport protocol (server mode only): -- `quic=`: QUIC transport mode (`0` for TCP pool, `1` for QUIC UDP pool, default: 0) +Connection pool type (server mode only): +- `type=`: Connection pool type (`0` for TCP pool, `1` for QUIC UDP pool, `2` for WebSocket/WSS pool, default: 0) - Server configuration is automatically delivered to client during handshake - - Client does not need to specify quic parameter + - Client does not need to specify type parameter ## Operating Modes @@ -51,7 +51,7 @@ NodePass offers three complementary operating modes to suit various deployment s Server mode establishes tunnel control channels and supports bidirectional data flow forwarding. ```bash -nodepass "server:///?log=&tls=&crt=&key=&dns=&quic=&max=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" +nodepass "server:///?log=&tls=&crt=&key=&dns=&type=&max=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" ``` #### Parameters @@ -60,9 +60,11 @@ nodepass "server:///?log=&tls=&crt=/?log=&dns=&quic=&min=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" +nodepass "client:///?log=&dns=&min=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" ``` #### Parameters @@ -149,7 +154,7 @@ nodepass "client:///?log=&dns=&quic=< - `notcp`: TCP support control (default: `0` enabled, `1` disabled) - `noudp`: UDP support control (default: `0` enabled, `1` disabled) -**Note**: QUIC transport configuration is automatically received from the server during handshake. Clients do not need to specify the `quic` parameter. +**Note**: Connection pool type configuration is automatically received from the server during handshake. Clients do not need to specify the `type` parameter. #### How Client Mode Works @@ -200,10 +205,10 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=16&log=inf # Resource-constrained configuration - Small connection pool nodepass "client://server.example.com:10101/127.0.0.1:8080?min=16&log=info" -# Client automatically receives QUIC configuration from server (no quic parameter needed) +# Client automatically receives pool type configuration from server (no type parameter needed) nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=debug" -# Client for real-time applications (QUIC config from server) +# Client for real-time applications (pool type config from server) nodepass "client://server.example.com:10101/127.0.0.1:7777?mode=2&min=64&read=30s" ``` diff --git a/nodepass/docs/zh/api.md b/nodepass/docs/zh/api.md index 89e0797a7f..f70f625faa 100644 --- a/nodepass/docs/zh/api.md +++ b/nodepass/docs/zh/api.md @@ -65,7 +65,7 @@ API Key 认证默认启用,首次启动自动生成并保存在 `nodepass.gob` "type": "client|server", "status": "running|stopped|error", "url": "...", - "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&quic=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&type=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", "restart": true, "meta": { "peer": { @@ -111,7 +111,7 @@ API Key 认证默认启用,首次启动自动生成并保存在 `nodepass.gob` - 服务端:`server://:/:?<参数>` - 客户端:`client://:/:?<参数>` -- 支持参数:`log`、`tls`、`crt`、`key`、`dns`、`min`、`max`、`mode`、`quic`、`dial`、`read`、`rate`、`slot`、`proxy`、`notcp`、`noudp` +- 支持参数:`log`、`tls`、`crt`、`key`、`dns`、`min`、`max`、`mode`、`type`、`dial`、`read`、`rate`、`slot`、`proxy`、`notcp`、`noudp` ### URL 查询参数 @@ -123,7 +123,7 @@ API Key 认证默认启用,首次启动自动生成并保存在 `nodepass.gob` - `mode`:运行模式控制(`0`、`1`、`2`)- 控制操作行为 - 对于服务端:`0`=自动,`1`=反向模式,`2`=正向模式 - 对于客户端:`0`=自动,`1`=单端转发,`2`=双端握手 -- `quic`:QUIC传输模式(`0`=TCP连接池,`1`=QUIC连接池,默认:`0`)- 仅服务端配置,客户端在握手时接收配置 +- `type`:连接池类型(`0`=TCP连接池,`1`=QUIC连接池,`2`=WebSocket/WSS连接池,默认:`0`)- 仅服务端配置,客户端在握手时接收配置 - `dial`:出站连接的源IP地址(默认:`auto`)- 仅服务端/客户端模式 - `read`:数据读取超时时长(如1h、30m、15s,默认:`0`表示无超时) - `rate`:带宽速率限制,单位Mbps(0=无限制) @@ -1116,7 +1116,7 @@ API响应中的实例对象包含以下字段: "type": "server", // 实例类型:server 或 client "status": "running", // 实例状态:running、stopped 或 error "url": "server://...", // 实例配置URL - "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&quic=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", // 完整配置URL + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&dns=5m&max=1024&mode=0&type=0&dial=auto&read=1h&rate=100&slot=65536&proxy=0¬cp=0&noudp=0", // 完整配置URL "restart": true, // 自启动策略 "meta": { // 用于组织和对端跟踪的元数据 "peer": { @@ -1335,7 +1335,7 @@ const instances = await fetch(`${API_URL}/instances`, { #### POST /instances - **描述**:创建新实例 - **认证**:需要API Key -- **请求体**:`{ "url": "client://或server://格式的URL" }` +- **请求体**:`{ "alias": "实例别名(可选)", "url": "client://或server://格式的URL" }` - **响应**:新创建的实例对象 - **示例**: ```javascript @@ -1345,7 +1345,10 @@ const newInstance = await fetch(`${API_URL}/instances`, { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, - body: JSON.stringify({ url: 'server://0.0.0.0:8080/localhost:3000' }) + body: JSON.stringify({ + alias: '我的服务器', + url: 'server://0.0.0.0:8080/localhost:3000' + }) }); ``` @@ -1582,14 +1585,16 @@ client://:/:? | `crt` | 证书路径 | 文件路径 | 无 | 仅服务端 | | `key` | 私钥路径 | 文件路径 | 无 | 仅服务端 | | `dns` | DNS缓存时间 | 时间长度 (如 `10m`, `30s`, `1h`) | `5m` | 两者 | +| `sni` | 主机名指示 | 主机名 | `none` | 仅客户端双端握手模式 | | `min` | 最小连接池容量 | 整数 > 0 | `64` | 仅客户端双端握手模式 | | `max` | 最大连接池容量 | 整数 > 0 | `1024` | 双端握手模式 | | `mode` | 运行模式控制 | `0`(自动), `1`(强制模式1), `2`(强制模式2) | `0` | 两者 | -| `quic` | QUIC协议支持 | `0`(禁用), `1`(启用) | `0` | 仅服务端 | +| `type` | 连接池类型 | `0`(TCP), `1`(QUIC), `2`(WebSocket), `3`(HTTP/2) | `0` | 仅服务端 | | `dial` | 出站源IP地址 | IP地址或 `auto` | `auto` | 两者 | | `read` | 读取超时时间 | 时间长度 (如 `10m`, `30s`, `1h`) | `0` | 两者 | | `rate` | 带宽速率限制 | 整数 (Mbps), 0=无限制 | `0` | 两者 | | `slot` | 连接槽位数 | 整数 (1-65536) | `65536` | 两者 | | `proxy` | PROXY协议支持 | `0`(禁用), `1`(启用) | `0` | 两者 | +| `block` | 协议屏蔽 | `0`(禁用), `1`(SOCKS), `2`(HTTP), `3`(TLS) | `0` | 两者 | | `notcp` | TCP支持控制 | `0`(启用), `1`(禁用) | `0` | 两者 | | `noudp` | UDP支持控制 | `0`(启用), `1`(禁用) | `0` | 两者 | \ No newline at end of file diff --git a/nodepass/docs/zh/configuration.md b/nodepass/docs/zh/configuration.md index ca8b5407de..a6e96a5ee0 100644 --- a/nodepass/docs/zh/configuration.md +++ b/nodepass/docs/zh/configuration.md @@ -213,59 +213,139 @@ nodepass "server://0.0.0.0:10101/remote.example.com:8080?log=info&tls=1&dial=10. - 绑定失败会触发自动回退以防止连接失败 - 此参数不影响传入的隧道连接或服务器监听地址 -## QUIC传输协议 +## 连接池类型 -NodePass支持QUIC作为双端握手模式下连接池的替代传输协议。QUIC提供基于UDP的多路复用流,具有内置加密和与传统TCP池相比更优的性能特征。 +NodePass支持三种连接池类型,用于双端握手模式下的隧道连接管理。每种类型都提供不同的传输协议和性能特征。 -- `quic`: QUIC传输模式(默认:0) +- `type`: 连接池类型(默认:0) - 值0:使用基于TCP的连接池(传统连接池库) - 值1:使用基于QUIC的连接池(UDP多路复用流) + - 值2:使用基于WebSocket/WSS的连接池(HTTP升级连接) + - 值3:使用基于HTTP/2的连接池(单TLS连接多路复用流) - 仅适用于双端握手模式(mode=2) - 如果尚未配置TLS则自动启用(最低tls=1) - - 在单个UDP连接上使用QUIC流进行多路复用连接 - 服务端配置在握手时自动下发给客户端 -**QUIC优势:** +### TCP连接池 (type=0) + +传统的基于TCP的连接池,提供最大的兼容性和可靠性。 + +**优势:** +- 最大的网络兼容性,几乎所有网络均支持TCP +- 严格TCP语义和顺序保证 +- 在低丢包率网络中性能稳定 +- 企业环境广泛接受 +- 在稳定网络中延迟低 + +**使用场景:** +- 阻止或限制UDP流量的网络 +- 需要严格TCP语义的应用 +- 企业环境中的默认选择 +- 最大兼容性需求 + +### QUIC连接池 (type=1) + +基于QUIC协议的现代连接池,提供基于UDP的多路复用流。 + +**优势:** - **多路复用**:在单个UDP连接上实现多个流 - **降低延迟**:通过0-RTT支持实现更快的连接建立 - **更好的丢包恢复**:流级别的流量控制和拥塞管理 - **NAT穿透**:基于UDP的协议在NAT和防火墙环境中表现更好 - **内置加密**:所有QUIC连接强制使用TLS 1.3加密 -**QUIC要求:** -- 仅需服务端配置`quic`参数 - 客户端自动接收配置 -- 必须启用TLS模式(tls=1或tls=2)- 如果quic=1会自动设置 -- 仅在双端握手模式下可用(mode=2或带远程地址的mode=0) -- 不适用于单端转发模式(mode=1) +**使用场景:** +- 移动网络或频繁变化的网络条件 +- 高延迟连接(卫星、长距离) +- NAT密集环境 +- 实时应用,受益于流独立性 +- 需要0-RTT重连的场景 + +**要求:** +- 必须启用TLS模式(tls=1或tls=2) +- 仅在双端握手模式下可用(mode=2) +- 需要UDP端口可访问 + +### WebSocket连接池 (type=2) + +基于WebSocket协议的连接池,通过HTTP升级建立连接。 + +**优势:** +- **代理穿透**:可以穿透HTTP代理和CDN +- **防火墙友好**:使用标准HTTP/HTTPS端口,容易通过防火墙 +- **Web基础设施兼容**:与现有Web基础设施集成 +- **双向通信**:支持全双工通信 +- **广泛支持**:所有现代浏览器和平台支持 + +**使用场景:** +- 需要穿透HTTP代理或CDN +- 企业环境仅允许HTTP/HTTPS流量 +- 防火墙阻止原始TCP连接 +- 需要与现有网络基础设施兼容 +- Web代理或VPN替代方案 + +**要求:** +- **必须启用TLS** - WebSocket连接池必须使用WSS(加密)。需要至少`tls=1`,生产环境推荐`tls=2` +- 仅在双端握手模式下可用(mode=2) +- 需要TCP端口支持WebSocket升级 +- **重要**:类型2不支持不加密模式(tls=0)。如果指定tls=0与type=2,系统将自动强制使用tls=1 + +### HTTP/2连接池 (type=3) + +基于HTTP/2协议的连接池,在单个TLS连接上提供多路复用流。 + +**优势:** +- **流多路复用**:在单个TCP连接上实现多个独立流 +- **头部压缩**:HPACK压缩减少带宽使用 +- **二进制协议**:高效的二进制帧减少解析开销 +- **流量控制**:每个流和连接级别的流量控制 +- **服务端推送**:优化数据传输模式的潜力 +- **TLS集成**:原生TLS 1.3支持与强加密 +- **防火墙友好**:使用标准HTTPS端口和协议模式 + +**使用场景:** +- 仅允许HTTP/HTTPS策略的企业环境 +- 需要协议级优化和效率的网络 +- 受益于流多路复用的高并发场景 +- 需要代理穿透和性能的环境 +- 需要精细流量控制的应用 +- 具有HTTP/2感知负载均衡器或代理的基础设施 + +**要求:** +- 必须启用TLS模式(tls=1或tls=2) +- 仅在双端握手模式下可用(mode=2) +- 需要HTTP/2协议支持(NodePass内置) + +### 配置示例 -示例: ```bash -# 使用QUIC传输的服务器(自动启用TLS) -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=1&mode=2" +# TCP连接池(默认) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=0&mode=2&tls=1" -# 客户端自动采用服务器的QUIC传输配置 +# QUIC连接池(自动启用TLS) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=1&mode=2" + +# WebSocket连接池(使用自定义TLS证书) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=2&tls=2&crt=/path/to/cert.pem&key=/path/to/key.pem" + +# HTTP/2连接池(带TLS的多路复用流) +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=3&mode=2&tls=1" + +# 客户端自动采用服务器的连接池类型配置 nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2" - -# 使用自定义TLS证书的QUIC(仅服务端配置) -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=1&tls=2&crt=/path/to/cert.pem&key=/path/to/key.pem" - -# 传统TCP连接池(默认行为) -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=0&mode=2" ``` -**QUIC使用场景:** -- **高延迟网络**:在卫星或长距离链路中减少连接开销 -- **移动网络**:更好地处理网络切换和丢包 -- **实时应用**:为游戏、VoIP或视频流降低延迟 -- **复杂NAT环境**:在复杂NAT场景中改善连接性 -- **并发流**:高效处理多个并行数据流 - **重要说明:** -- QUIC模式要求服务端和客户端的UDP端口可访问 -- 防火墙规则必须允许隧道端口上的UDP流量 -- 某些网络中间设备可能会阻止或降低UDP流量的优先级 -- QUIC连接使用保活和自动重连机制 -- 流多路复用在所有并发连接之间共享带宽 +- 仅需服务端配置`type`参数 - 客户端自动接收配置 +- **WebSocket连接池(type=2)需要TLS**:至少`tls=1`。如果type=2但未配置TLS,系统会自动设置tls=1 +- 所有连接池类型仅在双端握手模式下可用(mode=2或带远程地址的mode=0) +- 不适用于单端转发模式(mode=1) + +**连接池类型使用场景:** +- **TCP连接池**:标准企业环境、最大兼容性、稳定网络 +- **QUIC连接池**:高延迟网络、移动网络、实时应用、复杂NAT环境 +- **WebSocket连接池**:HTTP代理穿透、企业防火墙限制、Web基础设施集成 +- **HTTP/2连接池**:HTTP/HTTPS仅支持策略、高并发场景、协议级优化需求 ## 连接池容量参数 @@ -278,15 +358,15 @@ nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=0&mode=2" - 客户端设置的`max`参数会被服务端在握手时传递的值覆盖 - `min`参数由客户端完全控制,服务端不会修改此值 - 在客户端单端转发模式下,不使用连接池,这些参数被忽略 -- 适用于TCP连接池(quic=0)和QUIC连接池(quic=1) +- 适用于所有连接池类型(type=0, type=1, type=2) 示例: ```bash # 客户端设置最小连接池为32,最大连接池将由服务端决定 nodepass "client://server.example.com:10101/127.0.0.1:8080?min=32" -# 使用QUIC和自定义池容量的客户端 -nodepass "client://server.example.com:10101/127.0.0.1:8080?quic=1&min=128" +# 使用自定义池容量的客户端(连接池类型由服务端配置) +nodepass "client://server.example.com:10101/127.0.0.1:8080?min=128" ``` ## 数据读取超时 @@ -500,6 +580,100 @@ nodepass "server://0.0.0.0:10101/0.0.0.0:8080?log=info&tls=1&noudp=1" - 切换到noudp=1时,现有的UDP会话将被终止 - 当noudp=1时,UDP缓冲池和会话管理被禁用 +## 协议屏蔽 + +NodePass提供细粒度的协议屏蔽功能,防止特定协议通过隧道传输。这对于需要阻止某些协议同时允许其他协议的安全策略非常有用。 + +`block`参数使用数字字符串,其中每个数字代表一个协议类别: +- `1`:屏蔽SOCKS协议(SOCKS4/4a/5) +- `2`:屏蔽HTTP协议(所有HTTP方法) +- `3`:屏蔽TLS/SSL协议(加密连接) + +可以通过以任意顺序包含相应数字来屏蔽多个协议。参数值可以包含重复数字而不影响行为。 + +### 配置选项 + +- `block`:协议屏蔽控制(默认:未设置或`0`) + - 未设置或`0`:允许所有协议(不屏蔽) + - 包含`1`:屏蔽SOCKS4、SOCKS4a和SOCKS5协议 + - 包含`2`:屏蔽HTTP协议(GET、POST、CONNECT等) + - 包含`3`:屏蔽TLS/SSL握手(0x16内容类型) + +### 示例 + +仅屏蔽SOCKS协议: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=1" +``` + +仅屏蔽HTTP协议: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=2" +``` + +同时屏蔽SOCKS和HTTP: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=12" +# 或者 +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=21" +``` + +屏蔽所有三个协议类别: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?block=123" +# 或任意组合如:321、213、312等 +``` + +与其他安全设置结合: +```bash +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?log=info&tls=1&block=12&slot=1024" +``` + +### 检测机制 + +NodePass使用高效的协议检测,开销极小: + +- **SOCKS检测**:检查传入连接的前2个字节 + - SOCKS4/4a:检查版本字节`0x04`和命令字节`0x01`/`0x02` + - SOCKS5:检查版本字节`0x05`和有效的方法计数 + +- **HTTP检测**:扫描最多8个字节以查找HTTP方法模式 + - 识别大写字母后跟空格字符 + - 检测所有标准HTTP方法(GET、POST、CONNECT、DELETE等) + - 也检测自定义HTTP方法和WebDAV扩展 + +- **TLS检测**:检查第一个字节的TLS握手 + - 识别TLS握手记录类型`0x16` + - 阻止TLS 1.0、1.1、1.2和1.3握手 + +### 使用场景 + +**在隧道服务中屏蔽代理协议:** +```bash +# 仅允许应用流量,屏蔽代理协议 +nodepass "server://0.0.0.0:10101/app.backend.local:8080?block=12" +``` + +**执行加密策略:** +```bash +# 仅允许加密流量,屏蔽明文协议 +nodepass "server://0.0.0.0:10101/0.0.0.0:443?block=12&tls=2" +``` + +**防止TLS嵌套:** +```bash +# 当外层已加密时阻止嵌套TLS +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?tls=1&block=3" +``` + +### 重要说明 + +- 协议检测发生在连接建立时(接收到的第一批字节) +- 被屏蔽的连接会立即关闭并记录警告日志 +- 此功能增加的CPU开销极小(通常每个连接<0.1ms) +- 协议屏蔽适用于单端和双端转发模式 +- 与`notcp`/`noudp`结合使用可实现完整的流量控制 + ## 目标地址组与负载均衡 NodePass支持配置多个目标地址以实现高可用性和负载均衡。目标地址组功能仅适用于出口端(流量最终到达的目的地),不应在入口端使用。 @@ -561,24 +735,26 @@ nodepass "client://127.0.0.1:3306/db-primary.local:3306,db-secondary.local:3306? NodePass支持通过URL查询参数进行灵活配置,不同参数在 server、client、master 模式下的适用性如下表: -| 参数 | 说明 | 默认值 | server | client | master | -|-----------|----------------------|-------------------|:------:|:------:|:------:| -| `log` | 日志级别 | `info` | O | O | O | -| `tls` | TLS加密模式 | `0` | O | X | O | -| `crt` | 自定义证书路径 | N/A | O | X | O | -| `key` | 自定义密钥路径 | N/A | O | X | O | -| `dns` | DNS缓存TTL | `5m` | O | O | X | -| `min` | 最小连接池容量 | `64` | X | O | X | -| `max` | 最大连接池容量 | `1024` | O | X | X | -| `mode` | 运行模式控制 | `0` | O | O | X | -| `quic` | QUIC协议支持 | `0` | O | X | X | -| `dial` | 出站源IP地址 | `auto` | O | O | X | -| `read` | 数据读取超时 | `0` | O | O | X | -| `rate` | 带宽速率限制 | `0` | O | O | X | -| `slot` | 最大连接数限制 | `65536` | O | O | X | -| `proxy` | PROXY协议支持 | `0` | O | O | X | -| `notcp` | TCP支持控制 | `0` | O | O | X | -| `noudp` | UDP支持控制 | `0` | O | O | X | +| 参数 | 说明 | 默认值 | 可选值 | server | client | master | +|------|------|--------|--------|:------:|:------:|:------:| +| `log` | 日志级别 | `info` | `none`/`debug`/`info`/`warn`/`error`/`event` | O | O | O | +| `tls` | TLS加密模式 | `0` | `0`/`1`/`2` | O | X | O | +| `crt` | 自定义证书路径 | N/A | 文件路径 | O | X | O | +| `key` | 自定义密钥路径 | N/A | 文件路径 | O | X | O | +| `dns` | DNS缓存TTL | `5m` | `30s`/`5m`/`1h`等 | O | O | X | +| `sni` | 主机名指示 | `none` | 主机名 | X | O | X | +| `min` | 最小连接池容量 | `64` | 正整数 | X | O | X | +| `max` | 最大连接池容量 | `1024` | 正整数 | O | X | X | +| `mode` | 运行模式控制 | `0` | `0`/`1`/`2` | O | O | X | +| `type` | 连接池类型 | `0` | `0`/`1`/`2`/`3` | O | X | X | +| `dial` | 出站源IP地址 | `auto` | `auto`/IP地址 | O | O | X | +| `read` | 数据读取超时 | `0` | `0`/`30s`/`5m`等 | O | O | X | +| `rate` | 带宽速率限制 | `0` | `0`或正整数(Mbps) | O | O | X | +| `slot` | 最大连接数限制 | `65536` | `0`或正整数 | O | O | X | +| `proxy` | PROXY协议支持 | `0` | `0`/`1` | O | O | X | +| `block` | 协议屏蔽 | `0` | `0`/`1`/`2`/`3` | O | O | X | +| `notcp` | TCP支持控制 | `0` | `0`/`1` | O | O | X | +| `noudp` | UDP支持控制 | `0` | `0`/`1` | O | O | X | - O:参数有效,推荐根据实际场景配置 - X:参数无效,忽略设置 @@ -588,9 +764,10 @@ NodePass支持通过URL查询参数进行灵活配置,不同参数在 server、c - client/server 双端握手模式建议根据流量和资源情况调整连接池容量(min/max),优化性能。 - 当自动检测不符合部署需求时或需要跨环境一致行为时,使用运行模式控制(mode)。 - 配置速率限制(rate)以控制带宽使用,防止共享环境中的网络拥塞。 -- 配置QUIC传输(quic)时仅需在服务端设置 - 客户端在握手时自动接收配置。 +- 配置连接池类型(type)时仅需在服务端设置 - 客户端在握手时自动接收配置。 - 仅需要隧道传输UDP流量时设置`notcp=1`,以减少资源使用并简化配置。 - 仅需要隧道传输TCP流量时设置`noudp=1`,以减少资源使用并简化配置。 +- 使用`block`参数通过屏蔽特定协议类别(SOCKS/HTTP/TLS)来执行安全策略。 - 日志级别(log)可在所有模式下灵活调整,便于运维和排查。 ## 环境变量 diff --git a/nodepass/docs/zh/examples.md b/nodepass/docs/zh/examples.md index 747459622f..70487aee7e 100644 --- a/nodepass/docs/zh/examples.md +++ b/nodepass/docs/zh/examples.md @@ -137,9 +137,85 @@ nodepass client://service-a:10101/127.0.0.1:8082 - 将日志限制为仅警告和错误 - 使服务A的API在服务B上显示为本地服务 +## 协议屏蔽和流量过滤 + +### 示例9:屏蔽代理协议 + +阻止SOCKS和HTTP代理使用你的隧道: + +```bash +# 屏蔽SOCKS和HTTP代理协议的服务器 +nodepass "server://0.0.0.0:10101/app.backend.com:8080?block=12&tls=1" + +# 连接到受保护服务器的客户端 +nodepass "client://server.example.com:10101/127.0.0.1:8080" +``` + +此配置: +- 屏蔽所有SOCKS4/4a/5代理连接(`block`包含`1`) +- 屏蔽所有HTTP代理方法如CONNECT、GET、POST(`block`包含`2`) +- 仅允许应用特定协议通过隧道 +- 用于防止在应用隧道上滥用代理 + +### 示例10:屏蔽TLS嵌套场景 + +当外层已提供安全保护时,防止嵌套TLS加密: + +```bash +# 使用TLS加密并屏蔽内部TLS连接的服务器 +nodepass "server://0.0.0.0:10101/0.0.0.0:8080?tls=1&block=3" + +# 客户端自动继承TLS设置 +nodepass "client://server.example.com:10101/127.0.0.1:8080" +``` + +此设置: +- 使用TLS加密隧道本身(`tls=1`) +- 屏蔽加密隧道内的TLS握手(`block=3`) +- 防止不必要的双重加密开销 +- 有助于识别应用尝试添加冗余TLS的错误配置 + +### 示例11:综合安全策略 + +执行严格的安全策略,仅允许应用流量: + +```bash +# 具有综合协议屏蔽的生产服务器 +nodepass "server://0.0.0.0:10101/secure-app.internal:443?tls=2&crt=/path/to/cert.pem&key=/path/to/key.pem&block=123&slot=500" + +# 强制加密的客户端 +nodepass "client://prod-server.example.com:10101/127.0.0.1:8443?log=warn" +``` + +此配置: +- 使用经过验证的自定义证书以获得最大安全性(`tls=2`) +- 屏蔽SOCKS代理(`block`包含`1`) +- 屏蔽HTTP代理(`block`包含`2`) +- 屏蔽嵌套TLS连接(`block`包含`3`) +- 将并发连接限制为500以控制资源 +- 仅记录警告和错误以减少噪音 + +### 示例12:开发环境的选择性协议屏蔽 + +在开发环境中允许HTTP流量同时屏蔽代理: + +```bash +# 仅屏蔽SOCKS协议的开发服务器 +nodepass "server://127.0.0.1:10101/localhost:3000?block=1&log=debug" + +# 开发客户端 +nodepass "client://127.0.0.1:10101/localhost:8080" +``` + +此设置: +- 屏蔽SOCKS协议但允许HTTP请求 +- 适用于需要HTTP方法的Web应用测试 +- 防止开发人员隧道传输SOCKS代理流量 +- 启用调试日志记录以进行故障排除 + ## 带宽速率限制 -### 示例9:带速率限制的文件传输服务器 +### 示例13:带速率限制的文件传输服务器 控制文件传输服务的带宽使用: @@ -157,7 +233,7 @@ nodepass "client://fileserver.example.com:10101/127.0.0.1:3000?log=info&rate=50" - 允许文件传输的同时为其他服务保留带宽 - 使用TLS加密确保文件传输安全 -### 示例10:物联网传感器数据收集的保守限制 +### 示例14:物联网传感器数据收集的保守限制 对于带宽有限或按流量计费的物联网设备: @@ -175,7 +251,7 @@ nodepass "client://iot-gateway.example.com:10101/127.0.0.1:1883?log=error&rate=2 - 最小日志记录(warn/error)以减少物联网设备的资源使用 - 高效适用于MQTT或其他物联网协议 -### 示例11:开发环境速率控制 +### 示例15:开发环境速率控制 在带宽约束下测试应用程序: @@ -194,7 +270,7 @@ nodepass "server://0.0.0.0:10101/127.0.0.1:3000?log=debug&rate=500" ## 物联网设备管理 -### 示例12:物联网网关 +### 示例16:物联网网关 创建物联网设备的中央访问点: @@ -215,7 +291,7 @@ nodepass client://mgmt.example.com:10101/127.0.0.1:80 ## 多网卡系统与源IP控制 -### 示例13:指定网络接口选择 +### 示例17:指定网络接口选择 在多网卡系统上控制出站连接使用的网络接口: @@ -234,7 +310,7 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?dial=192.168.1.50&mod - 如果指定地址失败,自动回退到系统选择的IP - 支持IPv4和IPv6地址 -### 示例14:网络分段和VLAN路由 +### 示例18:网络分段和VLAN路由 通过特定网络段或VLAN引导流量: @@ -265,7 +341,7 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?dial=auto" ## DNS缓存TTL配置 -### 示例15:稳定的企业网络 +### 示例19:稳定的企业网络 为稳定的内部服务使用较长的TTL: @@ -283,7 +359,7 @@ nodepass "client://tunnel.corp.local:10101/127.0.0.1:8080?dns=1h" - 通过最小化DNS查找提高连接性能 - 适用于DNS稳定的生产环境 -### 示例16:动态DNS环境 +### 示例20:动态DNS环境 为频繁变化的DNS记录使用较短的TTL: @@ -301,7 +377,7 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?dns=30s" - 确保连接使用当前的DNS记录 - 适合IP频繁变化的云环境 -### 示例17:开发和测试 +### 示例21:开发和测试 为开发环境禁用缓存: @@ -319,7 +395,7 @@ nodepass "client://dev-server.local:10101/127.0.0.1:8080?dns=0&log=debug" - 在开发期间DNS记录频繁变化时很有用 - 帮助在测试期间识别DNS相关问题 -### 示例18:混合环境的自定义TTL +### 示例22:混合环境的自定义TTL 使用适中的TTL平衡性能和新鲜度: @@ -349,7 +425,7 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080" ## 高可用性与负载均衡 -### 示例19:多后端服务器负载均衡 +### 示例23:多后端服务器负载均衡 使用目标地址组实现流量均衡分配和自动故障转移: @@ -367,7 +443,7 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?log=info" - 故障服务器恢复后自动重新接入流量 - 使用TLS加密确保隧道安全 -### 示例20:数据库主从切换 +### 示例24:数据库主从切换 为数据库配置主从实例,实现高可用访问: @@ -382,7 +458,7 @@ nodepass "client://127.0.0.1:3306/db-primary.local:3306,db-secondary.local:3306? - 应用程序无需修改,透明地实现故障转移 - 仅记录警告和错误,减少日志输出 -### 示例21:API网关后端池 +### 示例25:API网关后端池 为API网关配置多个后端服务实例: @@ -400,7 +476,7 @@ nodepass "client://apigateway.example.com:10101/127.0.0.1:8080?rate=100&slot=200 - 客户端限制带宽100 Mbps,最大2000并发连接 - 单个实例故障不影响整体服务可用性 -### 示例22:地域分布式服务 +### 示例26:地域分布式服务 配置多地域服务节点,优化网络延迟: @@ -424,7 +500,7 @@ nodepass "server://0.0.0.0:10101/us-west.service:8080,us-east.service:8080,eu-ce ## PROXY协议集成 -### 示例23:负载均衡器与PROXY协议集成 +### 示例27:负载均衡器与PROXY协议集成 启用PROXY协议支持,与负载均衡器和反向代理集成: @@ -443,7 +519,7 @@ nodepass "client://tunnel.example.com:10101/127.0.0.1:3000?log=info&proxy=1" - 兼容HAProxy、Nginx和其他支持PROXY协议的服务 - 有助于维护准确的访问日志和基于IP的访问控制 -### 示例24:Web应用的反向代理支持 +### 示例28:Web应用的反向代理支持 使NodePass后的Web应用能够接收原始客户端信息: @@ -467,7 +543,7 @@ nodepass "server://0.0.0.0:10101/127.0.0.1:8080?log=warn&tls=2&crt=/path/to/cert - 支持连接审计的合规性要求 - 适用于支持PROXY协议的Web服务器(Nginx、HAProxy等) -### 示例25:数据库访问与客户端IP保留 +### 示例29:数据库访问与客户端IP保留 为数据库访问日志记录和安全维护客户端IP信息: @@ -494,7 +570,7 @@ nodepass "client://dbproxy.example.com:10101/127.0.0.1:5432?proxy=1" ## 容器部署 -### 示例26:容器化NodePass +### 示例30:容器化NodePass 在Docker环境中部署NodePass: @@ -529,7 +605,7 @@ docker run -d --name nodepass-client \ ## 主控API管理 -### 示例27:集中化管理 +### 示例31:集中化管理 为多个NodePass实例设置中央控制器: @@ -566,7 +642,7 @@ curl -X PUT http://localhost:9090/api/v1/instances/{id} \ - 提供用于自动化和集成的RESTful API - 包含内置的Swagger UI,位于http://localhost:9090/api/v1/docs -### 示例28:自定义API前缀 +### 示例32:自定义API前缀 为主控模式使用自定义API前缀: @@ -585,7 +661,7 @@ curl -X POST http://localhost:9090/admin/v1/instances \ - 用于安全或组织目的的自定义URL路径 - 在http://localhost:9090/admin/v1/docs访问Swagger UI -### 示例29:实时连接和流量监控 +### 示例33:实时连接和流量监控 通过主控API监控实例的连接数和流量统计: @@ -623,17 +699,17 @@ curl -H "X-API-Key: your-api-key" \ - **容量规划**:基于历史连接数据进行资源规划 - **故障诊断**:异常的连接数变化可能指示网络问题 -## QUIC传输协议 +## 连接池类型 -### 示例30: 基于QUIC的流多路复用隧道 +### 示例34: 基于QUIC的流多路复用隧道 使用QUIC协议进行连接池管理,在高延迟网络中提供更优性能: ```bash -# 服务器端:启用QUIC传输 -nodepass "server://0.0.0.0:10101/remote.example.com:8080?quic=1&mode=2&tls=1&log=debug" +# 服务器端:启用QUIC连接池 +nodepass "server://0.0.0.0:10101/remote.example.com:8080?type=1&mode=2&tls=1&log=debug" -# 客户端:自动从服务器接收QUIC配置 +# 客户端:自动从服务器接收连接池类型配置 nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=debug" ``` @@ -643,17 +719,17 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=de - 强制使用TLS 1.3加密(自动启用) - 在丢包场景中性能更好(无队头阻塞) - 通过0-RTT支持改善连接建立 -- 客户端在握手时自动接收服务器的QUIC配置 +- 客户端在握手时自动接收服务器的连接池类型配置 -### 示例31: 使用自定义TLS证书的QUIC +### 示例35: 使用自定义TLS证书的QUIC连接池 在生产环境部署带有验证证书的QUIC隧道: ```bash -# 服务器端:使用自定义证书的QUIC -nodepass "server://0.0.0.0:10101/backend.internal:8080?quic=1&mode=2&tls=2&crt=/etc/nodepass/cert.pem&key=/etc/nodepass/key.pem" +# 服务器端:使用自定义证书的QUIC连接池 +nodepass "server://0.0.0.0:10101/backend.internal:8080?type=1&mode=2&tls=2&crt=/etc/nodepass/cert.pem&key=/etc/nodepass/key.pem" -# 客户端:自动接收QUIC配置并进行证书验证 +# 客户端:自动接收连接池类型配置并进行证书验证 nodepass "client://tunnel.example.com:10101/127.0.0.1:8080?mode=2&min=64&log=info" ``` @@ -662,17 +738,62 @@ nodepass "client://tunnel.example.com:10101/127.0.0.1:8080?mode=2&min=64&log=inf - QUIC协议提供强制TLS 1.3加密 - 适用于生产环境 - 客户端进行完整证书验证 -- QUIC配置自动从服务器下发 +- 连接池类型配置自动从服务器下发 -### 示例32: 移动/高延迟网络的QUIC +### 示例36: WebSocket连接池穿透HTTP代理 + +在企业防火墙后使用WebSocket连接池: + +```bash +# 服务器端:启用WebSocket连接池(需要TLS) +nodepass "server://0.0.0.0:10101/internal.backend:8080?type=2&mode=2&tls=1&log=info" + +# 客户端:自动接收WebSocket配置 +nodepass "client://wss.tunnel.com:10101/127.0.0.1:8080?mode=2&min=64" +``` + +此配置: +- 使用WebSocket协议可以穿透HTTP代理和CDN +- **需要TLS加密** - 最少`tls=1`,生产环境建议使用带证书的`tls=2` +- 使用标准HTTPS端口,容易通过防火墙 +- 与现有Web基础设施兼容 +- 支持全双工通信 +- 适合企业环境中仅允许HTTP/HTTPS流量的场景 +- 客户端自动采用服务器的连接池类型配置 +- **注意**:WebSocket连接池不支持不加密模式(tls=0) + +### 示例37: 高并发环境的HTTP/2连接池 + +使用HTTP/2连接池实现高效的多路复用流和协议优化: + +```bash +# 服务器端:启用HTTP/2连接池(需要TLS) +nodepass "server://0.0.0.0:10101/backend.internal:8080?type=3&mode=2&tls=1&log=info" + +# 客户端:自动接收HTTP/2配置 +nodepass "client://h2.tunnel.com:10101/127.0.0.1:8080?mode=2&min=64" +``` + +此配置: +- 使用HTTP/2协议在单个TLS连接上实现多路复用流 +- **需要TLS加密** - 最少`tls=1`,生产环境建议使用带证书的`tls=2` +- HPACK头部压缩减少带宽使用 +- 高效解析的二进制帧协议 +- 每个流的流量控制实现最优资源利用 +- 与HTTP/2感知的代理和负载均衡器配合使用 +- 适合HTTP/HTTPS仅支持策略的环境 +- 客户端自动采用服务器的连接池类型配置 +- 适用于受益于流多路复用的高并发场景 + +### 示例38: 移动/高延迟网络的QUIC连接池 针对移动网络或卫星连接进行优化: ```bash -# 服务器端:带自适应池大小的QUIC -nodepass "server://0.0.0.0:10101/api.backend:443?quic=1&mode=2&max=512&tls=1&log=info" +# 服务器端:带自适应池大小的QUIC连接池 +nodepass "server://0.0.0.0:10101/api.backend:443?type=1&mode=2&max=512&tls=1&log=info" -# 客户端:自动接收QUIC,配置较大最小连接池用于移动网络 +# 客户端:自动接收连接池类型,配置较大最小连接池用于移动网络 nodepass "client://mobile.tunnel.com:10101/127.0.0.1:8080?mode=2&min=256&log=warn" ``` @@ -682,20 +803,28 @@ nodepass "client://mobile.tunnel.com:10101/127.0.0.1:8080?mode=2&min=256&log=war - 流多路复用减少连接开销 - 更好地处理丢包和抖动 - 0-RTT重连在网络变化后实现更快恢复 -- 客户端自动采用服务器的QUIC配置 +- 客户端自动采用服务器的连接池类型配置 -### 示例33: QUIC与TCP连接池性能对比 +### 示例39: 连接池类型性能对比 -QUIC和TCP连接池的并排比较: +TCP、QUIC、WebSocket和HTTP/2连接池的并排比较: ```bash # 传统TCP连接池(默认) -nodepass "server://0.0.0.0:10101/backend.example.com:8080?quic=0&mode=2&tls=1&log=event" +nodepass "server://0.0.0.0:10101/backend.example.com:8080?type=0&mode=2&tls=1&log=event" nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=event" # QUIC连接池(现代方法) -nodepass "server://0.0.0.0:10102/backend.example.com:8080?quic=1&mode=2&tls=1&log=event" +nodepass "server://0.0.0.0:10102/backend.example.com:8080?type=1&mode=2&tls=1&log=event" nodepass "client://server.example.com:10102/127.0.0.1:8081?mode=2&min=128&log=event" + +# WebSocket连接池(代理穿透) +nodepass "server://0.0.0.0:10103/backend.example.com:8080?type=2&mode=2&tls=1&log=event" +nodepass "client://server.example.com:10103/127.0.0.1:8082?mode=2&min=128&log=event" + +# HTTP/2连接池(多路复用流) +nodepass "server://0.0.0.0:10104/backend.example.com:8080?type=3&mode=2&tls=1&log=event" +nodepass "client://server.example.com:10104/127.0.0.1:8083?mode=2&min=128&log=event" ``` **TCP连接池优势**: @@ -710,15 +839,29 @@ nodepass "client://server.example.com:10102/127.0.0.1:8081?mode=2&min=128&log=ev - 改善NAT穿透能力 - 单个UDP套接字减少资源使用 -### 示例34: 实时应用的QUIC +**WebSocket连接池优势**: +- 可以穿透HTTP代理和CDN +- 使用标准HTTP/HTTPS端口 +- 与现有Web基础设施集成 +- 适合企业防火墙环境 + +**HTTP/2连接池优势**: +- 在单个TCP连接上高效的流多路复用 +- HPACK头部压缩减少带宽 +- 高效解析的二进制协议 +- 每个流的流量控制实现资源优化 +- 与HTTP/2感知的基础设施配合使用 +- 适合HTTP/HTTPS仅支持策略的环境 + +### 示例40: 实时应用的QUIC连接池 为游戏、VoIP或视频流配置QUIC隧道: ```bash # 服务器端:为实时流量优化的QUIC设置 -nodepass "server://0.0.0.0:10101/gameserver.local:7777?quic=1&mode=2&tls=1&read=30s&slot=10000" +nodepass "server://0.0.0.0:10101/gameserver.local:7777?type=1&mode=2&tls=1&read=30s&slot=10000" -# 客户端:自动从服务器接收QUIC配置 +# 客户端:自动从服务器接收连接池类型配置 nodepass "client://game.tunnel.com:10101/127.0.0.1:7777?mode=2&min=64&read=30s" ``` @@ -728,14 +871,13 @@ nodepass "client://game.tunnel.com:10101/127.0.0.1:7777?mode=2&min=64&read=30s" - 30秒读取超时快速检测陈旧连接 - 大槽位限制支持许多并发玩家/流 - 减少连接建立开销 -- 客户端自动采用服务器的QUIC配置 +- 客户端自动采用服务器的连接池类型配置 -**QUIC使用场景总结**: -- **移动网络**:更好地处理网络切换和丢包 -- **高延迟链路**:通过0-RTT和多路复用减少开销 -- **实时应用**:流独立性防止队头阻塞 -- **复杂NAT环境**:基于UDP的协议更可靠地穿透NAT -- **并发流**:在并行流之间高效共享带宽 +**连接池类型使用场景总结**: +- **TCP连接池**:标准企业环境、最大兼容性、稳定网络 +- **QUIC连接池**:移动网络、高延迟链路、实时应用、复杂NAT环境 +- **WebSocket连接池**:HTTP代理穿透、企业防火墙限制、Web基础设施集成 +- **HTTP/2连接池**:HTTP/HTTPS仅支持策略、高并发Web流量、与HTTP/2感知基础设施集成 ## 下一步 diff --git a/nodepass/docs/zh/how-it-works.md b/nodepass/docs/zh/how-it-works.md index 8c5d5d2480..955ad8ffa4 100644 --- a/nodepass/docs/zh/how-it-works.md +++ b/nodepass/docs/zh/how-it-works.md @@ -210,27 +210,33 @@ NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议: ## 连接池架构 -NodePass 实现了一个高效的连接池系统来管理网络连接,这是其性能优势的核心设计。NodePass支持两种连接池传输协议:传统的基于TCP的连接池和现代的基于QUIC的UDP连接池。 +NodePass 实现了一个高效的连接池系统来管理网络连接,这是其性能优势的核心设计。NodePass支持三种连接池传输协议:传统的基于TCP的连接池、现代的基于QUIC的UDP连接池和基于WebSocket的连接池。 ### 传输协议选择 -NodePass通过`quic`参数提供两种连接池传输选项: +NodePass通过`type`参数提供三种连接池传输选项: -1. **基于TCP的连接池 (quic=0, 默认)**: +1. **基于TCP的连接池 (type=0, 默认)**: - 由`pool`库管理的传统TCP连接 - 客户端和服务器之间的多个独立TCP连接 - 在单独的TCP连接上进行标准TLS加密 - 经过充分测试且广泛兼容的方法 -2. **基于QUIC的连接池 (quic=1)**: +2. **基于QUIC的连接池 (type=1)**: - 由`quic`库管理的基于UDP的多路复用流 - 单个QUIC连接承载多个并发流 - 强制使用TLS 1.3加密并支持0-RTT - 在高延迟和移动网络中性能优越 +3. **基于WebSocket的连接池 (type=2)**: + - 通过HTTP升级建立的WebSocket连接 + - 可以穿透HTTP代理和CDN + - 使用标准HTTPS端口 + - 适合企业环境和防火墙限制场景 + ### QUIC连接池架构 -当启用`quic=1`时,NodePass使用QUIC协议进行连接池管理,具有以下特性: +当启用`type=1`时,NodePass使用QUIC协议进行连接池管理,具有以下特性: **流多路复用**: - 单个UDP连接承载多个双向流 @@ -283,12 +289,69 @@ NodePass通过`quic`参数提供两种连接池传输选项: - 与多个TCP连接相比改善NAT穿透 - 在丢包场景中降低延迟(无队头阻塞) +### WebSocket连接池架构 + +当启用`type=2`时,NodePass使用WebSocket协议进行连接池管理,具有以下特性: + +**HTTP升级机制**: +- 通过标准HTTP升级请求建立WebSocket连接 +- 兼容HTTP/1.1代理和CDN +- 使用标准的80(ws)或443(wss)端口 +- 支持自定义HTTP头部用于身份验证和路由 + +**连接建立**: +- 服务器在TCP端口上监听HTTP请求 +- 客户端发起HTTP升级请求到WebSocket +- 握手完成后,连接升级为全双工WebSocket +- 服务器为每个传入连接分配唯一的连接ID + +**连接生命周期**: +1. **连接创建**(服务器端): + - 服务器接受来自授权客户端的HTTP升级请求 + - 验证WebSocket握手参数(Origin、协议等) + - 升级连接并将其添加到连接池 + - 生成连接ID发送给客户端用于关联 + +2. **连接检索**(客户端): + - 客户端通过控制通道接收连接ID + - 客户端从WebSocket连接池中检索对应连接 + - 连接被包装为`net.Conn`以保持兼容性 + - 连接用于与目标端点进行数据交换 + +3. **连接终止**: + - 数据交换完成后发送WebSocket关闭帧 + - 优雅关闭并正确清理底层TCP连接 + - 支持关闭原因码和描述信息 + +**动态管理**: +- 基于连接创建成功率调整连接池容量 +- 连接创建间隔根据连接池利用率自适应 +- 在最小/最大边界内自动连接容量扩缩 +- Ping/Pong帧维护连接健康状态 + +**安全特性**: +- 支持WSS(WebSocket Secure)通过TLS加密 +- WebSocket连接池需要启用TLS加密 +- 两种TLS模式支持: + - 模式1:自签名证书的WSS + - 模式2:生产环境完整证书验证的WSS +- Origin验证防止跨站WebSocket劫持 +- 支持自定义身份验证头部 + +**穿透优势**: +- 使用标准HTTP/HTTPS端口,易于穿透防火墙 +- 兼容企业HTTP代理和负载均衡器 +- 可通过CDN和反向代理部署 +- 与HTTP流量混合,降低检测和封锁风险 +- 需要启用TLS加密以确保安全 + ### 设计哲学 连接池的设计遵循"预热优于冷启动"的原则,通过预先建立连接消除网络延迟。这种设计理念借鉴了现代高性能服务器的最佳实践,将连接建立的成本分摊到系统启动阶段,而非在关键路径上承担这一开销。 -TCP和QUIC连接池共享这一理念,但实现方式不同: -- **TCP连接池**:预先建立多个TCP连接 +三种连接池类型共享这一理念,但实现方式不同: +- **TCP连接池**:预先建立多个独立TCP连接 - **QUIC连接池**:在单个QUIC连接上预先创建多个流 +- **WebSocket连接池**:预先建立多个WebSocket连接 ### 池设计 1. **池类型**: @@ -302,7 +365,10 @@ TCP和QUIC连接池共享这一理念,但实现方式不同: - 最小容量由客户端设置,确保客户端具备基础连接保障 - 最大容量由服务端在握手时统一下发,实现全局资源协调 - **间隔控制**:连接/流创建之间的基于时间的限流,防止网络资源过载 - - **连接工厂**:可定制的连接创建函数(TCP)或流管理(QUIC) + - **连接工厂**:可定制的连接创建函数 + - TCP模式:标准TCP拨号和TLS握手 + - QUIC模式:流管理和多路复用 + - WebSocket模式:HTTP升级和握手处理 ### 先进性设计 1. **零延迟连接**: @@ -323,6 +389,7 @@ TCP和QUIC连接池共享这一理念,但实现方式不同: - ID和连接存储在池中,采用写时复制和延迟删除策略 - **TCP模式**:创建带有可选TLS的独立TCP连接 - **QUIC模式**:在共享QUIC连接上打开双向流 + - **WebSocket模式**:通过HTTP升级建立WebSocket连接 2. **连接获取**: - 客户端使用连接ID检索连接,支持精确匹配和快速查找 diff --git a/nodepass/docs/zh/troubleshooting.md b/nodepass/docs/zh/troubleshooting.md index bd5c14122a..f4fc79a540 100644 --- a/nodepass/docs/zh/troubleshooting.md +++ b/nodepass/docs/zh/troubleshooting.md @@ -370,11 +370,11 @@ - 在生产环境中,建议将 `nodepass.gob` 定期备份到不同的存储位置 - 使用配置管理工具保存实例配置的文本形式备份 -## QUIC特定问题 +## 连接池类型问题 -### QUIC连接失败 +### QUIC连接池连接失败 -**症状**:启用`quic=1`时QUIC隧道无法建立。 +**症状**:启用`type=1`时QUIC连接池隧道无法建立。 **可能的原因和解决方案**: @@ -386,14 +386,14 @@ 2. **TLS配置问题** - QUIC需要启用TLS(至少`tls=1`) - - 如果设置了`quic=1`但禁用TLS,系统会自动启用`tls=1` + - 如果设置了`type=1`但禁用TLS,系统会自动启用`tls=1` - 生产环境使用`tls=2`和有效证书 - 检查QUIC连接的证书有效性 -3. **客户端-服务器QUIC不匹配** - - 服务器和客户端必须使用相同的`quic`设置 - - 带`quic=1`的服务器需要带`quic=1`的客户端 - - 带`quic=0`的服务器需要带`quic=0`的客户端 +3. **客户端-服务器连接池类型不匹配** + - 服务器和客户端必须使用相同的`type`设置 + - 带`type=1`的服务器需要带`type=1`的客户端 + - 带`type=0`的服务器需要带`type=0`的客户端 - 检查日志中的"QUIC connection not available"错误 4. **模式兼容性** @@ -401,16 +401,77 @@ - 单端转发模式(mode=1)不可用 - 如果模式不兼容,系统将回退到TCP连接池 -### QUIC性能问题 +### WebSocket连接池连接失败 -**症状**:QUIC隧道性能低于预期或不如TCP连接池。 +**症状**:启用`type=2`时WebSocket连接池隧道无法建立。 + +**可能的原因和解决方案**: + +1. **HTTP/WebSocket端口被阻止** + - 验证TCP端口支持WebSocket协议访问 + - 检查防火墙规则和代理配置 + - 某些代理或CDN可能干扰WebSocket升级 + - 使用WebSocket客户端工具测试连接性 + +2. **TLS配置问题** + - WebSocket Secure(WSS)需要启用TLS(至少`tls=1`) + - **WebSocket连接池不支持不加密模式** - type=2不允许使用tls=0 + - 如果设置了`type=2`但禁用TLS,系统将自动强制使用`tls=1` + - 生产环境使用`tls=2`和有效证书 + - 检查WSS连接的证书有效性 + +3. **客户端-服务器连接池类型不匹配** + - 服务器和客户端必须使用相同的`type`设置 + - 带`type=2`的服务器需要带`type=2`的客户端 + - 配置在握手时自动下发 + - 检查日志中的"WebSocket connection not available"错误 + +### HTTP/2连接池连接失败 + +**症状**:启用`type=3`时HTTP/2连接池隧道无法建立。 + +**可能的原因和解决方案**: + +1. **TCP端口或HTTP/2协议被阻止** + - 验证TCP端口支持HTTP/2协议访问 + - 检查防火墙规则和网络策略 + - 某些网络可能阻止或检查HTTPS流量 + - 使用支持HTTP/2的客户端工具测试连接性 + +2. **TLS配置问题** + - HTTP/2需要启用TLS(至少`tls=1`) + - 如果设置了`type=3`但禁用TLS,系统将自动强制使用`tls=1` + - 生产环境使用`tls=2`和有效证书 + - HTTP/2需要TLS 1.3且支持ALPN(应用层协议协商) + - 检查证书有效性和ALPN配置 + +3. **客户端-服务器连接池类型不匹配** + - 服务器和客户端必须使用相同的`type`设置 + - 带`type=3`的服务器需要带`type=3`的客户端 + - 配置在握手时自动下发 + - 检查日志中的"HTTP/2 connection not available"错误 + +4. **模式兼容性** + - HTTP/2连接池仅在双端握手模式(mode=2)下工作 + - 单端转发模式(mode=1)不可用 + - 如果模式不兼容,系统将回退到TCP连接池 + +5. **HTTP/2协议协商失败** + - 验证ALPN扩展已启用并协商"h2"协议 + - 某些较旧的TLS实现可能不支持ALPN + - 检查日志中的协议协商错误 + - 确保两端都支持基于TLS的HTTP/2 + +### QUIC连接池性能问题 + +**症状**:QUIC连接池隧道性能低于预期或不如TCP连接池。 **可能的原因和解决方案**: 1. **网络路径问题** - 某些网络会降低UDP流量的优先级或进行流量整形 - 检查网络中间设备是否干扰QUIC - - 考虑使用TCP连接池(`quic=0`)进行对比测试 + - 考虑使用TCP连接池(`type=0`)进行对比测试 - 监控丢包率 - QUIC在低丢包率下性能更好 2. **连接池容量配置** @@ -429,6 +490,29 @@ - 使用TCP和QUIC连接池进行对比测试 - 对于需要严格顺序的应用考虑使用TCP连接池 +### WebSocket连接池性能问题 + +**症状**:WebSocket连接池隧道性能低于预期。 + +**可能的原因和解决方案**: + +1. **代理/CDN开销** + - 通过代理的WebSocket连接可能增加延迟 + - 检查中间代理是否缓冲流量 + - 考虑使用TCP连接池(`type=0`)或QUIC连接池(`type=1`)进行对比测试 + - 直接连接通常比代理连接性能更好 + +2. **帧开销** + - WebSocket协议为每条消息添加帧开销 + - 较大的消息大小减少相对开销 + - 监控帧大小并根据需要调整应用行为 + - 在延迟和吞吐量之间取得平衡 + +3. **TLS握手开销** + - WSS需要为每个连接进行TLS握手 + - 使用连接池来分摊握手成本 + - 增加`min`和`max`参数以获得更好的性能 + ### QUIC流耗尽 **症状**:使用QUIC时出现"流不足"错误或连接超时。 @@ -453,16 +537,23 @@ - NAT超时可能断开UDP连接 - 调整NAT设置 - 如果网络延迟高,增加连接超时 -### QUIC与TCP连接池选择 +### 连接池类型选择 -**何时使用QUIC**(`quic=1`): +**何时使用QUIC连接池**(`type=1`): - 移动网络或频繁变化的网络条件 - 高延迟连接(卫星、长距离) - NAT密集环境,UDP穿透性能更好 - 受益于流独立性的实时应用 - 0-RTT重连提供价值的场景 -**何时使用TCP连接池**(`quic=0`): +**何时使用WebSocket连接池**(`type=2`): +- 需要穿透HTTP代理或CDN的场景 +- 企业环境中仅允许HTTP/HTTPS流量 +- 防火墙阻止原始TCP连接的环境 +- 需要与现有网络基础设施兼容 +- Web代理或VPN替代方案 + +**何时使用TCP连接池**(`type=0`): - 阻止或严重限制UDP流量的网络 - 需要严格TCP语义的应用 - 具有UDP限制的企业环境 @@ -472,12 +563,16 @@ **对比测试**: ```bash # 测试TCP连接池性能 -nodepass "server://0.0.0.0:10101/backend:8080?quic=0&mode=2&log=event" -nodepass "client://server:10101/127.0.0.1:8080?quic=0&mode=2&log=event" +nodepass "server://0.0.0.0:10101/backend:8080?type=0&mode=2&log=event" +nodepass "client://server:10101/127.0.0.1:8080?mode=2&log=event" # 测试QUIC连接池性能 -nodepass "server://0.0.0.0:10102/backend:8080?quic=1&mode=2&log=event" -nodepass "client://server:10102/127.0.0.1:8081?quic=1&mode=2&log=event" +nodepass "server://0.0.0.0:10102/backend:8080?type=1&mode=2&log=event" +nodepass "client://server:10102/127.0.0.1:8081?mode=2&log=event" + +# 测试WebSocket连接池性能 +nodepass "server://0.0.0.0:10103/backend:8080?type=2&mode=2&log=event" +nodepass "client://server:10103/127.0.0.1:8082?mode=2&log=event" ``` 监控流量统计并根据观察到的性能进行选择。 diff --git a/nodepass/docs/zh/usage.md b/nodepass/docs/zh/usage.md index 3fa47533d0..81bcd332b9 100644 --- a/nodepass/docs/zh/usage.md +++ b/nodepass/docs/zh/usage.md @@ -7,7 +7,7 @@ NodePass创建一个带有未加密TCP控制通道的隧道,并为数据交换 NodePass命令的一般语法是: ```bash -nodepass ":///?log=&tls=&crt=&key=&dns=&min=&max=&mode=&quic=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" +nodepass ":///?log=&tls=&crt=&key=&dns=&min=&max=&mode=&type=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" ``` 其中: @@ -23,7 +23,7 @@ nodepass ":///?log=&tls=&crt=`:最小连接池容量(默认:64,由客户端设置) - `max=`:最大连接池容量(默认:1024,由服务端设置并下发给客户端) - `mode=`:运行模式控制(`0`、`1` 或 `2`)- 控制操作行为 -- `quic=`:QUIC传输模式(`0`为TCP连接池,`1`为QUIC UDP连接池,默认:0,仅服务端配置) +- `type=`:连接池类型(`0`为TCP连接池,`1`为QUIC UDP连接池,`2`为WebSocket/WSS连接池,`3`为HTTP/2连接池,默认:0,仅服务端配置) - `dial=`:出站连接的源IP地址(默认:`auto`,支持IPv4和IPv6) - `read=`:数据读取超时时长(默认:0,支持时间单位如30s、5m、1h等) - `rate=`:带宽速率限制,单位Mbps(默认:0表示无限制) @@ -37,10 +37,10 @@ TLS相关参数(仅适用于server/master模式): - `crt=`:证书文件路径(当`tls=2`时) - `key=`:私钥文件路径(当`tls=2`时) -QUIC传输协议(仅服务端模式): -- `quic=`:QUIC传输模式(`0`为TCP连接池,`1`为QUIC UDP连接池,默认:0) +连接池类型(仅服务端模式): +- `type=`:连接池类型(`0`为TCP连接池,`1`为QUIC UDP连接池,`2`为WebSocket/WSS连接池,默认:0) - 服务端配置在握手时自动下发给客户端 - - 客户端无需指定quic参数 + - 客户端无需指定type参数 ## 运行模式 @@ -51,7 +51,7 @@ NodePass提供三种互补的运行模式,以适应各种部署场景。 服务端模式建立隧道控制通道,并支持双向数据流转发。 ```bash -nodepass "server:///?log=&tls=&crt=&key=&dns=&quic=&max=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" +nodepass "server:///?log=&tls=&crt=&key=&dns=&type=&max=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" ``` #### 参数 @@ -60,9 +60,11 @@ nodepass "server:///?log=&tls=&crt=/?log=&dns=&quic=&min=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" +nodepass "client:///?log=&dns=&min=&mode=&dial=&read=&rate=&slot=&proxy=¬cp=<0|1>&noudp=<0|1>" ``` #### 参数 @@ -149,7 +154,7 @@ nodepass "client:///?log=&dns=&quic=< - `notcp`:TCP支持控制(默认:`0`启用,`1`禁用) - `noudp`:UDP支持控制(默认:`0`启用,`1`禁用) -**注意**:QUIC传输配置在握手时自动从服务器接收。客户端无需指定`quic`参数。 +**注意**:连接池类型配置在握手时自动从服务器接收。客户端无需指定`type`参数。 #### 客户端模式工作原理 @@ -200,10 +205,10 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=16&log=inf # 资源受限配置 - 小型连接池 nodepass "client://server.example.com:10101/127.0.0.1:8080?min=16&log=info" -# 客户端自动从服务器接收QUIC配置(无需quic参数) +# 客户端自动从服务器接收连接池类型配置(无需type参数) nodepass "client://server.example.com:10101/127.0.0.1:8080?mode=2&min=128&log=debug" -# 实时应用客户端(从服务器获取QUIC配置) +# 实时应用客户端(从服务器获取连接池类型配置) nodepass "client://server.example.com:10101/127.0.0.1:7777?mode=2&min=64&read=30s" ``` diff --git a/nodepass/go.mod b/nodepass/go.mod index 4145e3b6ee..9ac1b1e5e8 100644 --- a/nodepass/go.mod +++ b/nodepass/go.mod @@ -6,13 +6,17 @@ require ( github.com/NodePassProject/cert v1.0.1 github.com/NodePassProject/conn v1.0.16 github.com/NodePassProject/logs v1.0.3 - github.com/NodePassProject/pool v1.0.49 - github.com/NodePassProject/quic v1.0.8 + github.com/NodePassProject/nph2 v1.0.4 + github.com/NodePassProject/npws v1.0.5 + github.com/NodePassProject/pool v1.0.50 + github.com/NodePassProject/quic v1.0.14 ) require ( - github.com/quic-go/quic-go v0.57.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/quic-go/quic-go v0.57.1 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect ) diff --git a/nodepass/go.sum b/nodepass/go.sum index b2aad8acc9..c201918163 100644 --- a/nodepass/go.sum +++ b/nodepass/go.sum @@ -4,26 +4,34 @@ github.com/NodePassProject/conn v1.0.16 h1:ojHfyBveZMcyOikdUs1SOW4yKp92NOBnNhfNe github.com/NodePassProject/conn v1.0.16/go.mod h1:xfQ7ZLUxrtdLsljGHYYCToW+Hdg6DAbmL1Cs94n5h6E= github.com/NodePassProject/logs v1.0.3 h1:CDUZVQ477vmmFQHazrQCWM0gJPNINm0C2N3FzC4jVyw= github.com/NodePassProject/logs v1.0.3/go.mod h1:TwtPXOzLtb8iH+fdduQjEEywICXivsM39cy9AinMSks= -github.com/NodePassProject/pool v1.0.49 h1:gktVmE+GsQ0/C0MF8qgRraR7eS3na4k0QrQfR6o4fkM= -github.com/NodePassProject/pool v1.0.49/go.mod h1:joQFk1oocg56QpJ1QK/2g5Jv/AyqYUQgPXMG1gWe8iA= -github.com/NodePassProject/quic v1.0.8 h1:S3kmxInIk6UiN0DcbRqSSoOMOY+JTUdTeeRuAr1SNxs= -github.com/NodePassProject/quic v1.0.8/go.mod h1:4t2Y+iSKCvtuA0bpG2YgU03f9crkTUpM4LzUK4RG6XY= +github.com/NodePassProject/nph2 v1.0.4 h1:szvp7hlvMRt1g/g1hF++0DfgM++5yqWWAPlfMDh226c= +github.com/NodePassProject/nph2 v1.0.4/go.mod h1:fzNbTk0zh+0gXer2aKpPXF69o4/BOd15Ys2gvicwWR0= +github.com/NodePassProject/npws v1.0.5 h1:UpVm4UYytNQHq6Nkli4Qqq17wpuwvKqoCY571+vubBA= +github.com/NodePassProject/npws v1.0.5/go.mod h1:cyVS2X3/8f07yOkVEl53D+ozHLljJvHQx/s6xoLl/s8= +github.com/NodePassProject/pool v1.0.50 h1:Xmvb0hSHq24U06moHICr8CkqEHB4ZHBbY9Z+a7fSejQ= +github.com/NodePassProject/pool v1.0.50/go.mod h1:joQFk1oocg56QpJ1QK/2g5Jv/AyqYUQgPXMG1gWe8iA= +github.com/NodePassProject/quic v1.0.14 h1:zx5/cmQ8Tp1ntwCVAmtosODfBWTXTHHuprZdAspSjc8= +github.com/NodePassProject/quic v1.0.14/go.mod h1:JEHezuTn+AWN5NURNl2efa5lVqG6VQeutTBKhf9Y0T8= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= -github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/nodepass/internal/client.go b/nodepass/internal/client.go index 43f495a65e..a30e4acb11 100644 --- a/nodepass/internal/client.go +++ b/nodepass/internal/client.go @@ -2,31 +2,28 @@ package internal import ( - "bufio" "context" + "encoding/json" "fmt" "io" "net" + "net/http" "net/url" "os" "os/signal" - "strconv" - "strings" "sync" "syscall" "time" - "github.com/NodePassProject/conn" "github.com/NodePassProject/logs" + "github.com/NodePassProject/nph2" + "github.com/NodePassProject/npws" "github.com/NodePassProject/pool" "github.com/NodePassProject/quic" ) // Client 实现客户端模式功能 -type Client struct { - Common // 继承共享功能 - tunnelName string // 隧道名称 -} +type Client struct{ Common } // NewClient 创建新的客户端实例 func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) { @@ -51,7 +48,6 @@ func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) { pingURL: &url.URL{Scheme: "np", Fragment: "i"}, pongURL: &url.URL{Scheme: "np", Fragment: "o"}, }, - tunnelName: parsedURL.Hostname(), } if err := client.initConfig(); err != nil { return nil, fmt.Errorf("newClient: initConfig failed: %w", err) @@ -63,10 +59,10 @@ func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) { // Run 管理客户端生命周期 func (c *Client) Run() { logInfo := func(prefix string) { - c.logger.Info("%v: client://%v@%v/%v?dns=%v&min=%v&mode=%v&quic=%v&dial=%v&read=%v&rate=%v&slot=%v&proxy=%v¬cp=%v&noudp=%v", - prefix, c.tunnelKey, c.tunnelTCPAddr, c.getTargetAddrsString(), c.dnsCacheTTL, c.minPoolCapacity, - c.runMode, c.quicMode, c.dialerIP, c.readTimeout, c.rateLimit/125000, c.slotLimit, - c.proxyProtocol, c.disableTCP, c.disableUDP) + c.logger.Info("%v: client://%v@%v/%v?dns=%v&sni=%v&min=%v&mode=%v&dial=%v&read=%v&rate=%v&slot=%v&proxy=%v&block=%v¬cp=%v&noudp=%v", + prefix, c.tunnelKey, c.tunnelTCPAddr, c.getTargetAddrsString(), c.dnsCacheTTL, c.serverName, c.minPoolCapacity, + c.runMode, c.dialerIP, c.readTimeout, c.rateLimit/125000, c.slotLimit, + c.proxyProtocol, c.blockProtocol, c.disableTCP, c.disableUDP) } logInfo("Client started") @@ -140,51 +136,22 @@ func (c *Client) singleStart() error { // commonStart 启动双端握手模式 func (c *Client) commonStart() error { - // 与隧道服务端进行握手 + // 发起隧道握手 + c.logger.Info("Pending tunnel handshake...") + c.handshakeStart = time.Now() if err := c.tunnelHandshake(); err != nil { return fmt.Errorf("commonStart: tunnelHandshake failed: %w", err) } // 初始化连接池 - switch c.quicMode { - case "0": - tcpPool := pool.NewClientPool( - c.minPoolCapacity, - c.maxPoolCapacity, - minPoolInterval, - maxPoolInterval, - reportInterval, - c.tlsCode, - c.tunnelName, - func() (net.Conn, error) { - tcpAddr, err := c.getTunnelTCPAddr() - if err != nil { - return nil, err - } - return net.DialTimeout("tcp", tcpAddr.String(), tcpDialTimeout) - }) - go tcpPool.ClientManager() - c.tunnelPool = tcpPool - case "1": - udpPool := quic.NewClientPool( - c.minPoolCapacity, - c.maxPoolCapacity, - minPoolInterval, - maxPoolInterval, - reportInterval, - c.tlsCode, - c.tunnelName, - func() (string, error) { - udpAddr, err := c.getTunnelUDPAddr() - if err != nil { - return "", err - } - return udpAddr.String(), nil - }) - go udpPool.ClientManager() - c.tunnelPool = udpPool - default: - return fmt.Errorf("commonStart: unknown quic mode: %s", c.quicMode) + if err := c.initTunnelPool(); err != nil { + return fmt.Errorf("commonStart: initTunnelPool failed: %w", err) + } + + // 设置控制连接 + c.logger.Info("Getting tunnel pool ready...") + if err := c.setControlConn(); err != nil { + return fmt.Errorf("commonStart: setControlConn failed: %w", err) } // 判断数据流向 @@ -199,64 +166,126 @@ func (c *Client) commonStart() error { if err := c.commonControl(); err != nil { return fmt.Errorf("commonStart: commonControl failed: %w", err) } + + return nil +} + +// initTunnelPool 初始化隧道连接池 +func (c *Client) initTunnelPool() error { + switch c.poolType { + case "0": + tcpPool := pool.NewClientPool( + c.minPoolCapacity, + c.maxPoolCapacity, + minPoolInterval, + maxPoolInterval, + reportInterval, + c.tlsCode, + c.serverName, + func() (net.Conn, error) { + tcpAddr, err := c.getTunnelTCPAddr() + if err != nil { + return nil, err + } + return net.DialTimeout("tcp", tcpAddr.String(), tcpDialTimeout) + }) + go tcpPool.ClientManager() + c.tunnelPool = tcpPool + case "1": + quicPool := quic.NewClientPool( + c.minPoolCapacity, + c.maxPoolCapacity, + minPoolInterval, + maxPoolInterval, + reportInterval, + c.tlsCode, + c.serverName, + func() (string, error) { + udpAddr, err := c.getTunnelUDPAddr() + if err != nil { + return "", err + } + return udpAddr.String(), nil + }) + go quicPool.ClientManager() + c.tunnelPool = quicPool + case "2": + websocketPool := npws.NewClientPool( + c.minPoolCapacity, + c.maxPoolCapacity, + minPoolInterval, + maxPoolInterval, + reportInterval, + c.tlsCode, + c.tunnelAddr) + go websocketPool.ClientManager() + c.tunnelPool = websocketPool + case "3": + http2Pool := nph2.NewClientPool( + c.minPoolCapacity, + c.maxPoolCapacity, + minPoolInterval, + maxPoolInterval, + reportInterval, + c.tlsCode, + c.serverName, + func() (string, error) { + tcpAddr, err := c.getTunnelTCPAddr() + if err != nil { + return "", err + } + return tcpAddr.String(), nil + }) + go http2Pool.ClientManager() + c.tunnelPool = http2Pool + default: + return fmt.Errorf("initTunnelPool: unknown pool type: %s", c.poolType) + } return nil } // tunnelHandshake 与隧道服务端进行握手 func (c *Client) tunnelHandshake() error { - // 建立隧道TCP连接 - tunnelTCPAddr, err := c.getTunnelTCPAddr() + scheme := "http" + if c.serverPort == "443" { + scheme = "https" + } + + // 构建请求 + req, _ := http.NewRequest(http.MethodGet, scheme+"://"+c.tunnelAddr+"/", nil) + req.Host = c.serverName + req.Header.Set("Authorization", "Bearer "+c.generateAuthToken()) + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) if err != nil { - return fmt.Errorf("tunnelHandshake: getTunnelTCPAddr failed: %w", err) + return fmt.Errorf("tunnelHandshake: %w", err) } - tunnelTCPConn, err := net.DialTimeout("tcp", tunnelTCPAddr.String(), tcpDialTimeout) - if err != nil { - return fmt.Errorf("tunnelHandshake: dialTimeout failed: %w", err) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("tunnelHandshake: status %d", resp.StatusCode) } - c.tunnelTCPConn = tunnelTCPConn.(*net.TCPConn) - c.bufReader = bufio.NewReader(&conn.TimeoutReader{Conn: c.tunnelTCPConn, Timeout: 3 * reportInterval}) - c.tunnelTCPConn.SetKeepAlive(true) - c.tunnelTCPConn.SetKeepAlivePeriod(reportInterval) - - // 发送隧道密钥 - _, err = c.tunnelTCPConn.Write(c.encode([]byte(c.tunnelKey))) - if err != nil { - return fmt.Errorf("tunnelHandshake: write tunnel key failed: %w", err) + // 解析配置 + var config struct { + Flow string `json:"flow"` + Max int `json:"max"` + TLS string `json:"tls"` + Type string `json:"type"` + } + if err := json.NewDecoder(resp.Body).Decode(&config); err != nil { + return fmt.Errorf("tunnelHandshake: %w", err) } - // 读取隧道URL - rawTunnelURL, err := c.bufReader.ReadBytes('\n') - if err != nil { - return fmt.Errorf("tunnelHandshake: readBytes failed: %w", err) - } + // 更新配置 + c.dataFlow = config.Flow + c.maxPoolCapacity = config.Max + c.tlsCode = config.TLS + c.poolType = config.Type - // 解码隧道URL - tunnelURLData, err := c.decode(rawTunnelURL) - if err != nil { - return fmt.Errorf("tunnelHandshake: decode tunnel URL failed: %w", err) - } - - // 解析隧道URL - tunnelURL, err := url.Parse(string(tunnelURLData)) - if err != nil { - return fmt.Errorf("tunnelHandshake: parse tunnel URL failed: %w", err) - } - - // 更新客户端配置 - if tunnelURL.User.Username() == "" || tunnelURL.Host == "" || tunnelURL.Path == "" || tunnelURL.Fragment == "" { - return net.UnknownNetworkError(tunnelURL.String()) - } - c.quicMode = tunnelURL.User.Username() - if max, err := strconv.Atoi(tunnelURL.Host); err != nil { - return fmt.Errorf("tunnelHandshake: parse max pool capacity failed: %w", err) - } else { - c.maxPoolCapacity = max - } - c.dataFlow = strings.TrimPrefix(tunnelURL.Path, "/") - c.tlsCode = tunnelURL.Fragment - - c.logger.Info("Tunnel signal <- : %v <- %v", tunnelURL.String(), c.tunnelTCPConn.RemoteAddr()) - c.logger.Info("Tunnel handshaked: %v <-> %v", c.tunnelTCPConn.LocalAddr(), c.tunnelTCPConn.RemoteAddr()) + c.logger.Info("Loading tunnel config: FLOW=%v|MAX=%v|TLS=%v|TYPE=%v", + c.dataFlow, c.maxPoolCapacity, c.tlsCode, c.poolType) return nil } diff --git a/nodepass/internal/common.go b/nodepass/internal/common.go index f18f5e1561..d6f532cb5c 100644 --- a/nodepass/internal/common.go +++ b/nodepass/internal/common.go @@ -5,6 +5,7 @@ import ( "bufio" "bytes" "context" + "crypto/hmac" "crypto/sha256" "crypto/tls" "encoding/base64" @@ -36,8 +37,11 @@ type Common struct { tlsConfig *tls.Config // TLS配置 coreType string // 核心类型 runMode string // 运行模式 - quicMode string // QUIC模式 + poolType string // 连接池类型 dataFlow string // 数据流向 + serverName string // 服务器名称 + serverPort string // 服务器端口 + clientIP string // 客户端地址 dialerIP string // 拨号本地IP dialerFallback uint32 // 拨号回落标志 tunnelKey string // 隧道密钥 @@ -50,7 +54,7 @@ type Common struct { targetIdx uint64 // 目标地址索引 targetListener *net.TCPListener // 目标监听器 tunnelListener net.Listener // 隧道监听器 - tunnelTCPConn *net.TCPConn // 隧道TCP连接 + controlConn net.Conn // 隧道控制连接 tunnelUDPConn *conn.StatConn // 隧道UDP连接 targetUDPConn *conn.StatConn // 目标UDP连接 targetUDPSession sync.Map // 目标UDP会话 @@ -58,6 +62,10 @@ type Common struct { minPoolCapacity int // 最小池容量 maxPoolCapacity int // 最大池容量 proxyProtocol string // 代理协议 + blockProtocol string // 屏蔽协议 + blockSOCKS bool // 屏蔽SOCKS协议 + blockHTTP bool // 屏蔽HTTP协议 + blockTLS bool // 屏蔽TLS协议 disableTCP string // 禁用TCP disableUDP string // 禁用UDP rateLimit int // 速率限制 @@ -67,6 +75,7 @@ type Common struct { tcpBufferPool *sync.Pool // TCP缓冲区池 udpBufferPool *sync.Pool // UDP缓冲区池 signalChan chan string // 信号通道 + handshakeStart time.Time // 握手开始时间 checkPoint time.Time // 检查点时间 flushURL *url.URL // 重置信号 pingURL *url.URL // PING信号 @@ -89,6 +98,17 @@ type dnsCacheEntry struct { expiredAt time.Time } +// readerConn 包装自定义读取器 +type readerConn struct { + net.Conn + reader io.Reader +} + +// Read 实现自定义读 +func (rc *readerConn) Read(b []byte) (int, error) { + return rc.reader.Read(b) +} + // TransportPool 统一连接池接口 type TransportPool interface { IncomingGet(timeout time.Duration) (string, net.Conn, error) @@ -122,20 +142,23 @@ var ( ReloadInterval = getEnvAsDuration("NP_RELOAD_INTERVAL", 1*time.Hour) // 重载间隔 ) -// 默认配置 +// 常量定义 const ( - defaultDNSTTL = 5 * time.Minute // 默认DNS缓存TTL - defaultMinPool = 64 // 默认最小池容量 - defaultMaxPool = 1024 // 默认最大池容量 - defaultRunMode = "0" // 默认运行模式 - defaultQuicMode = "0" // 默认QUIC模式 - defaultDialerIP = "auto" // 默认拨号本地IP - defaultReadTimeout = 0 * time.Second // 默认读取超时 - defaultRateLimit = 0 // 默认速率限制 - defaultSlotLimit = 65536 // 默认槽位限制 - defaultProxyProtocol = "0" // 默认代理协议 - defaultTCPStrategy = "0" // 默认TCP策略 - defaultUDPStrategy = "0" // 默认UDP策略 + contextCheckInterval = 50 * time.Millisecond // 上下文检查间隔 + defaultDNSTTL = 5 * time.Minute // 默认DNS缓存TTL + defaultMinPool = 64 // 默认最小池容量 + defaultMaxPool = 1024 // 默认最大池容量 + defaultServerName = "none" // 默认服务器名称 + defaultRunMode = "0" // 默认运行模式 + defaultPoolType = "0" // 默认连接池类型 + defaultDialerIP = "auto" // 默认拨号本地IP + defaultReadTimeout = 0 * time.Second // 默认读取超时 + defaultRateLimit = 0 // 默认速率限制 + defaultSlotLimit = 65536 // 默认槽位限制 + defaultProxyProtocol = "0" // 默认代理协议 + defaultBlockProtocol = "0" // 默认协议屏蔽 + defaultTCPStrategy = "0" // 默认TCP策略 + defaultUDPStrategy = "0" // 默认UDP策略 ) // getTCPBuffer 获取TCP缓冲区 @@ -244,6 +267,16 @@ func (c *Common) xor(data []byte) []byte { return data } +// generateAuthToken 生成认证令牌 +func (c *Common) generateAuthToken() string { + return hex.EncodeToString(hmac.New(sha256.New, []byte(c.tunnelKey)).Sum(nil)) +} + +// verifyAuthToken 验证认证令牌 +func (c *Common) verifyAuthToken(token string) bool { + return hmac.Equal([]byte(token), []byte(c.generateAuthToken())) +} + // encode base64编码数据 func (c *Common) encode(data []byte) []byte { return append([]byte(base64.StdEncoding.EncodeToString(c.xor(data))), '\n') @@ -448,6 +481,9 @@ func (c *Common) getAddress() error { // 保存原始隧道地址 c.tunnelAddr = tunnelAddr + if name, port, err := net.SplitHostPort(tunnelAddr); err == nil { + c.serverName, c.serverPort = name, port + } // 解析隧道TCP地址 tcpAddr, err := c.resolveAddr("tcp", tunnelAddr) @@ -545,6 +581,17 @@ func (c *Common) getDNSTTL() { } } +// getServerName 获取服务器名称 +func (c *Common) getServerName() { + if serverName := c.parsedURL.Query().Get("sni"); serverName != "" { + c.serverName = serverName + return + } + if c.serverName == "" || net.ParseIP(c.serverName) != nil { + c.serverName = defaultServerName + } +} + // getPoolCapacity 获取连接池容量设置 func (c *Common) getPoolCapacity() { if min := c.parsedURL.Query().Get("min"); min != "" { @@ -573,14 +620,14 @@ func (c *Common) getRunMode() { } } -// getQuicMode 获取QUIC模式 -func (c *Common) getQuicMode() { - if quicMode := c.parsedURL.Query().Get("quic"); quicMode != "" { - c.quicMode = quicMode +// getPoolType 获取连接池类型 +func (c *Common) getPoolType() { + if poolType := c.parsedURL.Query().Get("type"); poolType != "" { + c.poolType = poolType } else { - c.quicMode = defaultQuicMode + c.poolType = defaultPoolType } - if c.quicMode != "0" && c.tlsCode == "0" { + if c.poolType == "1" && c.tlsCode == "0" { c.tlsCode = "1" } } @@ -640,6 +687,18 @@ func (c *Common) getProxyProtocol() { } } +// getBlockProtocol 获取屏蔽协议设置 +func (c *Common) getBlockProtocol() { + if protocol := c.parsedURL.Query().Get("block"); protocol != "" { + c.blockProtocol = protocol + } else { + c.blockProtocol = defaultBlockProtocol + } + c.blockSOCKS = strings.Contains(c.blockProtocol, "1") + c.blockHTTP = strings.Contains(c.blockProtocol, "2") + c.blockTLS = strings.Contains(c.blockProtocol, "3") +} + // getTCPStrategy 获取TCP策略 func (c *Common) getTCPStrategy() { if tcpStrategy := c.parsedURL.Query().Get("notcp"); tcpStrategy != "" { @@ -668,13 +727,15 @@ func (c *Common) initConfig() error { c.getDNSTTL() c.getTunnelKey() c.getPoolCapacity() + c.getServerName() c.getRunMode() - c.getQuicMode() + c.getPoolType() c.getDialerIP() c.getReadTimeout() c.getRateLimit() c.getSlotLimit() c.getProxyProtocol() + c.getBlockProtocol() c.getTCPStrategy() c.getUDPStrategy() @@ -718,6 +779,48 @@ func (c *Common) sendProxyV1Header(ip string, conn net.Conn) error { return nil } +// detectBlockProtocol 检测屏蔽协议 +func (c *Common) detectBlockProtocol(conn net.Conn) (string, net.Conn) { + if !c.blockSOCKS && !c.blockHTTP && !c.blockTLS { + return "", conn + } + + reader := bufio.NewReader(conn) + b, err := reader.Peek(8) + if err != nil || len(b) < 1 { + return "", &readerConn{Conn: conn, reader: reader} + } + + // 检测SOCKS + if c.blockSOCKS && len(b) >= 2 { + if b[0] == 0x04 && (b[1] == 0x01 || b[1] == 0x02) { + return "SOCKS4", &readerConn{Conn: conn, reader: reader} + } + if b[0] == 0x05 && b[1] >= 0x01 && b[1] <= 0x03 { + return "SOCKS5", &readerConn{Conn: conn, reader: reader} + } + } + + // 检测HTTP + if c.blockHTTP && len(b) >= 4 && b[0] >= 'A' && b[0] <= 'Z' { + for i, c := range b[1:] { + if c == ' ' { + return "HTTP", &readerConn{Conn: conn, reader: reader} + } + if c < 'A' || c > 'Z' || i >= 7 { + break + } + } + } + + // 检测TLS + if c.blockTLS && b[0] == 0x16 { + return "TLS", &readerConn{Conn: conn, reader: reader} + } + + return "", &readerConn{Conn: conn, reader: reader} +} + // initRateLimiter 初始化全局限速器 func (c *Common) initRateLimiter() { if c.rateLimit > 0 { @@ -833,10 +936,10 @@ func (c *Common) stop() { c.logger.Debug("Tunnel connection closed: %v", c.tunnelUDPConn.LocalAddr()) } - // 关闭隧道TCP连接 - if c.tunnelTCPConn != nil { - c.tunnelTCPConn.Close() - c.logger.Debug("Tunnel connection closed: %v", c.tunnelTCPConn.LocalAddr()) + // 关闭隧道控制连接 + if c.controlConn != nil { + c.controlConn.Close() + c.logger.Debug("Control connection closed: %v", c.controlConn.LocalAddr()) } // 关闭目标监听器 @@ -879,6 +982,33 @@ func (c *Common) shutdown(ctx context.Context, stopFunc func()) error { } } +// setControlConn 设置控制连接 +func (c *Common) setControlConn() error { + start := time.Now() + for c.ctx.Err() == nil { + if c.tunnelPool.Ready() && c.tunnelPool.Active() > 0 { + break + } + if time.Since(start) > handshakeTimeout { + return fmt.Errorf("setControlConn: handshake timeout") + } + select { + case <-c.ctx.Done(): + return fmt.Errorf("setControlConn: context error: %w", c.ctx.Err()) + case <-time.After(contextCheckInterval): + } + } + + poolConn, err := c.tunnelPool.OutgoingGet("00000000", poolGetTimeout) + if err != nil { + return fmt.Errorf("setControlConn: outgoingGet failed: %w", err) + } + c.controlConn = poolConn + c.bufReader = bufio.NewReader(&conn.TimeoutReader{Conn: c.controlConn, Timeout: 3 * reportInterval}) + c.logger.Info("Marking tunnel handshake as complete in %vms", time.Since(c.handshakeStart).Milliseconds()) + return nil +} + // commonControl 共用控制逻辑 func (c *Common) commonControl() error { errChan := make(chan error, 3) @@ -912,7 +1042,7 @@ func (c *Common) commonQueue() error { select { case <-c.ctx.Done(): return fmt.Errorf("commonQueue: context error: %w", c.ctx.Err()) - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } @@ -926,7 +1056,7 @@ func (c *Common) commonQueue() error { select { case <-c.ctx.Done(): return fmt.Errorf("commonQueue: context error: %w", c.ctx.Err()) - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } } } @@ -958,8 +1088,8 @@ func (c *Common) healthCheck() error { // 连接池健康度检查 if c.tunnelPool.ErrorCount() > c.tunnelPool.Active()/2 { // 发送刷新信号到对端 - if c.ctx.Err() == nil && c.tunnelTCPConn != nil { - _, err := c.tunnelTCPConn.Write(c.encode([]byte(c.flushURL.String()))) + if c.ctx.Err() == nil && c.controlConn != nil { + _, err := c.controlConn.Write(c.encode([]byte(c.flushURL.String()))) if err != nil { c.mu.Unlock() return fmt.Errorf("healthCheck: write flush signal failed: %w", err) @@ -979,8 +1109,8 @@ func (c *Common) healthCheck() error { // 发送PING信号 c.checkPoint = time.Now() - if c.ctx.Err() == nil && c.tunnelTCPConn != nil { - _, err := c.tunnelTCPConn.Write(c.encode([]byte(c.pingURL.String()))) + if c.ctx.Err() == nil && c.controlConn != nil { + _, err := c.controlConn.Write(c.encode([]byte(c.pingURL.String()))) if err != nil { c.mu.Unlock() return fmt.Errorf("healthCheck: write ping signal failed: %w", err) @@ -1007,7 +1137,7 @@ func (c *Common) incomingVerify() { select { case <-c.ctx.Done(): continue - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } } @@ -1032,21 +1162,21 @@ func (c *Common) incomingVerify() { // 构建并发送验证信号 verifyURL := &url.URL{ Scheme: "np", - Host: c.tunnelTCPConn.RemoteAddr().String(), + Host: c.controlConn.RemoteAddr().String(), Path: url.PathEscape(id), Fragment: "v", // TLS验证 } - if c.ctx.Err() == nil && c.tunnelTCPConn != nil { + if c.ctx.Err() == nil && c.controlConn != nil { c.mu.Lock() - _, err = c.tunnelTCPConn.Write(c.encode([]byte(verifyURL.String()))) + _, err = c.controlConn.Write(c.encode([]byte(verifyURL.String()))) c.mu.Unlock() if err != nil { return } } - c.logger.Debug("TLS verify signal: cid %v -> %v", id, c.tunnelTCPConn.RemoteAddr()) + c.logger.Debug("TLS verify signal: cid %v -> %v", id, c.controlConn.RemoteAddr()) } // commonLoop 共用处理循环 @@ -1066,7 +1196,7 @@ func (c *Common) commonLoop() { select { case <-c.ctx.Done(): return - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } } } @@ -1085,7 +1215,7 @@ func (c *Common) commonTCPLoop() { select { case <-c.ctx.Done(): return - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } @@ -1108,6 +1238,14 @@ func (c *Common) commonTCPLoop() { defer c.releaseSlot(false) + // 阻止屏蔽协议 + protocol, wrappedConn := c.detectBlockProtocol(targetConn) + if protocol != "" { + c.logger.Warn("commonTCPLoop: blocked %v protocol from %v", protocol, targetConn.RemoteAddr()) + return + } + targetConn = wrappedConn + // 从连接池获取连接 id, remoteConn, err := c.tunnelPool.IncomingGet(poolGetTimeout) if err != nil { @@ -1135,9 +1273,9 @@ func (c *Common) commonTCPLoop() { Fragment: "1", // TCP模式 } - if c.ctx.Err() == nil && c.tunnelTCPConn != nil { + if c.ctx.Err() == nil && c.controlConn != nil { c.mu.Lock() - _, err = c.tunnelTCPConn.Write(c.encode([]byte(launchURL.String()))) + _, err = c.controlConn.Write(c.encode([]byte(launchURL.String()))) c.mu.Unlock() if err != nil { @@ -1146,7 +1284,7 @@ func (c *Common) commonTCPLoop() { } } - c.logger.Debug("TCP launch signal: cid %v -> %v", id, c.tunnelTCPConn.RemoteAddr()) + c.logger.Debug("TCP launch signal: cid %v -> %v", id, c.controlConn.RemoteAddr()) buffer1 := c.getTCPBuffer() buffer2 := c.getTCPBuffer() @@ -1180,7 +1318,7 @@ func (c *Common) commonUDPLoop() { select { case <-c.ctx.Done(): return - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } @@ -1266,9 +1404,9 @@ func (c *Common) commonUDPLoop() { Fragment: "2", // UDP模式 } - if c.ctx.Err() == nil && c.tunnelTCPConn != nil { + if c.ctx.Err() == nil && c.controlConn != nil { c.mu.Lock() - _, err = c.tunnelTCPConn.Write(c.encode([]byte(launchURL.String()))) + _, err = c.controlConn.Write(c.encode([]byte(launchURL.String()))) c.mu.Unlock() if err != nil { c.logger.Error("commonUDPLoop: write launch signal failed: %v", err) @@ -1276,7 +1414,7 @@ func (c *Common) commonUDPLoop() { } } - c.logger.Debug("UDP launch signal: cid %v -> %v", id, c.tunnelTCPConn.RemoteAddr()) + c.logger.Debug("UDP launch signal: cid %v -> %v", id, c.controlConn.RemoteAddr()) c.logger.Debug("Starting transfer: %v <-> %v", remoteConn.LocalAddr(), c.targetUDPConn.LocalAddr()) } @@ -1306,7 +1444,7 @@ func (c *Common) commonOnce() error { select { case <-c.ctx.Done(): return fmt.Errorf("commonOnce: context error: %w", c.ctx.Err()) - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } @@ -1322,7 +1460,7 @@ func (c *Common) commonOnce() error { select { case <-c.ctx.Done(): return fmt.Errorf("commonOnce: context error: %w", c.ctx.Err()) - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } @@ -1355,9 +1493,9 @@ func (c *Common) commonOnce() error { c.logger.Debug("Tunnel pool flushed: %v active connections", c.tunnelPool.Active()) }() case "i": // PING - if c.ctx.Err() == nil && c.tunnelTCPConn != nil { + if c.ctx.Err() == nil && c.controlConn != nil { c.mu.Lock() - _, err := c.tunnelTCPConn.Write(c.encode([]byte(c.pongURL.String()))) + _, err := c.controlConn.Write(c.encode([]byte(c.pongURL.String()))) c.mu.Unlock() if err != nil { return fmt.Errorf("commonOnce: write pong signal failed: %w", err) @@ -1388,7 +1526,7 @@ func (c *Common) outgoingVerify(signalURL *url.URL) { select { case <-c.ctx.Done(): continue - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } } @@ -1399,7 +1537,7 @@ func (c *Common) outgoingVerify(signalURL *url.URL) { } else { id = unescapedID } - c.logger.Debug("TLS verify signal: cid %v <- %v", id, c.tunnelTCPConn.RemoteAddr()) + c.logger.Debug("TLS verify signal: cid %v <- %v", id, c.controlConn.RemoteAddr()) testConn, err := c.tunnelPool.OutgoingGet(id, poolGetTimeout) if err != nil { @@ -1435,7 +1573,7 @@ func (c *Common) commonTCPOnce(signalURL *url.URL) { } else { id = unescapedID } - c.logger.Debug("TCP launch signal: cid %v <- %v", id, c.tunnelTCPConn.RemoteAddr()) + c.logger.Debug("TCP launch signal: cid %v <- %v", id, c.controlConn.RemoteAddr()) // 从连接池获取连接 remoteConn, err := c.tunnelPool.OutgoingGet(id, poolGetTimeout) @@ -1508,7 +1646,7 @@ func (c *Common) commonUDPOnce(signalURL *url.URL) { } else { id = unescapedID } - c.logger.Debug("UDP launch signal: cid %v <- %v", id, c.tunnelTCPConn.RemoteAddr()) + c.logger.Debug("UDP launch signal: cid %v <- %v", id, c.controlConn.RemoteAddr()) // 获取池连接 remoteConn, err := c.tunnelPool.OutgoingGet(id, poolGetTimeout) @@ -1718,7 +1856,7 @@ func (c *Common) singleTCPLoop() error { select { case <-c.ctx.Done(): return fmt.Errorf("singleTCPLoop: context error: %w", c.ctx.Err()) - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } @@ -1741,6 +1879,14 @@ func (c *Common) singleTCPLoop() error { defer c.releaseSlot(false) + // 阻止屏蔽协议 + protocol, wrappedConn := c.detectBlockProtocol(tunnelConn) + if protocol != "" { + c.logger.Warn("singleTCPLoop: blocked %v protocol from %v", protocol, tunnelConn.RemoteAddr()) + return + } + tunnelConn = wrappedConn + // 尝试建立目标连接 targetConn, err := c.dialWithRotation("tcp", tcpDialTimeout) if err != nil { @@ -1761,6 +1907,7 @@ func (c *Common) singleTCPLoop() error { c.logger.Error("singleTCPLoop: sendProxyV1Header failed: %v", err) return } + buffer1 := c.getTCPBuffer() buffer2 := c.getTCPBuffer() defer func() { @@ -1795,7 +1942,7 @@ func (c *Common) singleUDPLoop() error { select { case <-c.ctx.Done(): return fmt.Errorf("singleUDPLoop: context error: %w", c.ctx.Err()) - case <-time.After(50 * time.Millisecond): + case <-time.After(contextCheckInterval): } continue } diff --git a/nodepass/internal/master.go b/nodepass/internal/master.go index b8769e5f8e..864b82d49b 100644 --- a/nodepass/internal/master.go +++ b/nodepass/internal/master.go @@ -1038,7 +1038,8 @@ func (m *Master) handleInstances(w http.ResponseWriter, r *http.Request) { case http.MethodPost: // 创建新实例 var reqData struct { - URL string `json:"url"` + Alias string `json:"alias"` + URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil || reqData.URL == "" { httpError(w, "Invalid request body", http.StatusBadRequest) @@ -1069,6 +1070,7 @@ func (m *Master) handleInstances(w http.ResponseWriter, r *http.Request) { // 创建实例 instance := &Instance{ ID: id, + Alias: reqData.Alias, Type: instanceType, URL: m.enhanceURL(reqData.URL, instanceType), Status: "stopped", @@ -1785,10 +1787,13 @@ func (m *Master) generateConfigURL(instance *Instance) string { // 根据实例类型设置默认参数 switch instance.Type { case "client": - // client参数: dns, min, mode, dial, read, rate, slot, proxy, notcp, noudp + // client参数: dns, sni, min, mode, dial, read, rate, slot, proxy, block, notcp, noudp if query.Get("dns") == "" { query.Set("dns", defaultDNSTTL.String()) } + if query.Get("sni") == "" { + query.Set("sni", defaultServerName) + } if query.Get("min") == "" { query.Set("min", strconv.Itoa(defaultMinPool)) } @@ -1810,6 +1815,9 @@ func (m *Master) generateConfigURL(instance *Instance) string { if query.Get("proxy") == "" { query.Set("proxy", defaultProxyProtocol) } + if query.Get("block") == "" { + query.Set("block", defaultBlockProtocol) + } if query.Get("notcp") == "" { query.Set("notcp", defaultTCPStrategy) } @@ -1817,7 +1825,7 @@ func (m *Master) generateConfigURL(instance *Instance) string { query.Set("noudp", defaultUDPStrategy) } case "server": - // server参数: dns, max, mode, quic, dial, read, rate, slot, proxy, notcp, noudp + // server参数: dns, max, mode, type, dial, read, rate, slot, proxy, block, notcp, noudp if query.Get("dns") == "" { query.Set("dns", defaultDNSTTL.String()) } @@ -1827,8 +1835,8 @@ func (m *Master) generateConfigURL(instance *Instance) string { if query.Get("mode") == "" { query.Set("mode", defaultRunMode) } - if query.Get("quic") == "" { - query.Set("quic", defaultQuicMode) + if query.Get("type") == "" { + query.Set("type", defaultPoolType) } if query.Get("dial") == "" { query.Set("dial", defaultDialerIP) @@ -1845,6 +1853,9 @@ func (m *Master) generateConfigURL(instance *Instance) string { if query.Get("proxy") == "" { query.Set("proxy", defaultProxyProtocol) } + if query.Get("block") == "" { + query.Set("block", defaultBlockProtocol) + } if query.Get("notcp") == "" { query.Set("notcp", defaultTCPStrategy) } @@ -2087,7 +2098,10 @@ func (m *Master) generateOpenAPISpec() string { "CreateInstanceRequest": { "type": "object", "required": ["url"], - "properties": {"url": {"type": "string", "description": "Command string(scheme://host:port/host:port)"}} + "properties": { + "alias": {"type": "string", "description": "Instance alias"}, + "url": {"type": "string", "description": "Command string(scheme://host:port/host:port)"} + } }, "UpdateInstanceRequest": { "type": "object", diff --git a/nodepass/internal/server.go b/nodepass/internal/server.go index b38a663198..6d9bf25146 100644 --- a/nodepass/internal/server.go +++ b/nodepass/internal/server.go @@ -2,31 +2,30 @@ package internal import ( - "bufio" "context" "crypto/tls" + "encoding/json" "fmt" "io" "net" + "net/http" "net/url" "os" "os/signal" - "strconv" + "strings" "sync" "syscall" "time" - "github.com/NodePassProject/conn" "github.com/NodePassProject/logs" + "github.com/NodePassProject/nph2" + "github.com/NodePassProject/npws" "github.com/NodePassProject/pool" "github.com/NodePassProject/quic" ) // Server 实现服务端模式功能 -type Server struct { - Common // 继承共享功能 - clientIP string // 客户端IP -} +type Server struct{ Common } // NewServer 创建新的服务端实例 func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger *logs.Logger) (*Server, error) { @@ -64,10 +63,10 @@ func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger // Run 管理服务端生命周期 func (s *Server) Run() { logInfo := func(prefix string) { - s.logger.Info("%v: server://%v@%v/%v?dns=%v&max=%v&mode=%v&quic=%v&dial=%v&read=%v&rate=%v&slot=%v&proxy=%v¬cp=%v&noudp=%v", + s.logger.Info("%v: server://%v@%v/%v?dns=%v&max=%v&mode=%v&type=%v&dial=%v&read=%v&rate=%v&slot=%v&proxy=%v&block=%v¬cp=%v&noudp=%v", prefix, s.tunnelKey, s.tunnelTCPAddr, s.getTargetAddrsString(), s.dnsCacheTTL, s.maxPoolCapacity, - s.runMode, s.quicMode, s.dialerIP, s.readTimeout, s.rateLimit/125000, s.slotLimit, - s.proxyProtocol, s.disableTCP, s.disableUDP) + s.runMode, s.poolType, s.dialerIP, s.readTimeout, s.rateLimit/125000, s.slotLimit, + s.proxyProtocol, s.blockProtocol, s.disableTCP, s.disableUDP) } logInfo("Server started") @@ -115,6 +114,11 @@ func (s *Server) start() error { return fmt.Errorf("start: initTunnelListener failed: %w", err) } + // 关闭UDP监听器 + if s.tunnelUDPConn != nil { + s.tunnelUDPConn.Close() + } + // 运行模式判断 switch s.runMode { case "1": // 反向模式 @@ -134,38 +138,22 @@ func (s *Server) start() error { } } - // 与客户端进行握手 + // 接受隧道握手 + s.logger.Info("Pending tunnel handshake...") + s.handshakeStart = time.Now() if err := s.tunnelHandshake(); err != nil { return fmt.Errorf("start: tunnelHandshake failed: %w", err) } - // 握手之后把UDP监听关掉 - if s.tunnelUDPConn != nil { - s.tunnelUDPConn.Close() + // 初始化连接池 + if err := s.initTunnelPool(); err != nil { + return fmt.Errorf("start: initTunnelPool failed: %w", err) } - // 初始化隧道连接池 - switch s.quicMode { - case "0": - tcpPool := pool.NewServerPool( - s.maxPoolCapacity, - s.clientIP, - s.tlsConfig, - s.tunnelListener, - reportInterval) - go tcpPool.ServerManager() - s.tunnelPool = tcpPool - case "1": - udpPool := quic.NewServerPool( - s.maxPoolCapacity, - s.clientIP, - s.tlsConfig, - s.tunnelUDPAddr.String(), - reportInterval) - go udpPool.ServerManager() - s.tunnelPool = udpPool - default: - return fmt.Errorf("start: unknown quic mode: %s", s.quicMode) + // 设置控制连接 + s.logger.Info("Getting tunnel pool ready...") + if err := s.setControlConn(); err != nil { + return fmt.Errorf("start: setControlConn failed: %w", err) } // 判断数据流向 @@ -180,138 +168,110 @@ func (s *Server) start() error { return nil } -// tunnelHandshake 与客户端进行握手 -func (s *Server) tunnelHandshake() error { - type handshakeResult struct { - conn *net.TCPConn - bufReader *bufio.Reader - clientIP string +// initTunnelPool 初始化隧道连接池 +func (s *Server) initTunnelPool() error { + switch s.poolType { + case "0": + tcpPool := pool.NewServerPool( + s.maxPoolCapacity, + s.clientIP, + s.tlsConfig, + s.tunnelListener, + reportInterval) + go tcpPool.ServerManager() + s.tunnelPool = tcpPool + case "1": + quicPool := quic.NewServerPool( + s.maxPoolCapacity, + s.clientIP, + s.tlsConfig, + s.tunnelUDPAddr.String(), + reportInterval) + go quicPool.ServerManager() + s.tunnelPool = quicPool + case "2": + websocketPool := npws.NewServerPool( + s.maxPoolCapacity, + "", + s.tlsConfig, + s.tunnelListener, + reportInterval) + go websocketPool.ServerManager() + s.tunnelPool = websocketPool + case "3": + http2Pool := nph2.NewServerPool( + s.maxPoolCapacity, + s.clientIP, + s.tlsConfig, + s.tunnelListener, + reportInterval) + go http2Pool.ServerManager() + s.tunnelPool = http2Pool + default: + return fmt.Errorf("initTunnelPool: unknown pool type: %s", s.poolType) } - - successChan := make(chan handshakeResult, 1) - closeChan := make(chan struct{}) - var wg sync.WaitGroup - - go func() { - for { - select { - case <-closeChan: - return - default: - } - - // 接受隧道连接 - rawConn, err := s.tunnelListener.Accept() - if err != nil { - select { - case <-closeChan: - return - default: - continue - } - } - - // 并发处理握手 - wg.Add(1) - go func(rawConn net.Conn) { - defer wg.Done() - - select { - case <-closeChan: - rawConn.Close() - return - default: - } - - bufReader := bufio.NewReader(rawConn) - peek, err := bufReader.Peek(4) - if err == nil && len(peek) == 4 && peek[3] == ' ' { - clientIP := rawConn.RemoteAddr().(*net.TCPAddr).IP.String() + "\n" - if peek[0] == 'G' && peek[1] == 'E' && peek[2] == 'T' { - fmt.Fprintf(rawConn, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", len(clientIP), clientIP) - } else { - fmt.Fprint(rawConn, "HTTP/1.1 405 Method Not Allowed\r\nAllow: GET\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") - } - rawConn.Close() - return - } - - // 读取隧道密钥 - rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout)) - rawTunnelKey, err := bufReader.ReadBytes('\n') - if err != nil { - s.logger.Warn("tunnelHandshake: read timeout: %v", rawConn.RemoteAddr()) - rawConn.Close() - return - } - rawConn.SetReadDeadline(time.Time{}) - - // 解码隧道密钥 - tunnelKeyData, err := s.decode(rawTunnelKey) - if err != nil { - s.logger.Warn("tunnelHandshake: decode failed: %v", rawConn.RemoteAddr()) - rawConn.Close() - return - } - - // 验证隧道密钥 - if string(tunnelKeyData) != s.tunnelKey { - s.logger.Warn("tunnelHandshake: access denied: %v", rawConn.RemoteAddr()) - rawConn.Close() - return - } - - tcpConn := rawConn.(*net.TCPConn) - tcpConn.SetKeepAlive(true) - tcpConn.SetKeepAlivePeriod(reportInterval) - - // 返回握手结果 - select { - case successChan <- handshakeResult{ - conn: tcpConn, - bufReader: bufio.NewReader(&conn.TimeoutReader{Conn: tcpConn, Timeout: 3 * reportInterval}), - clientIP: tcpConn.RemoteAddr().(*net.TCPAddr).IP.String(), - }: - close(closeChan) - case <-closeChan: - rawConn.Close() - } - }(rawConn) - } - }() - - // 阻塞等待握手结果 - var result handshakeResult - select { - case result = <-successChan: - wg.Wait() - case <-s.ctx.Done(): - close(closeChan) - wg.Wait() - return fmt.Errorf("tunnelHandshake: context error: %w", s.ctx.Err()) - } - - // 保存握手结果 - s.tunnelTCPConn = result.conn - s.bufReader = result.bufReader - s.clientIP = result.clientIP - - // 构建隧道配置信息 - tunnelURL := &url.URL{ - Scheme: "np", - User: url.User(s.quicMode), - Host: strconv.Itoa(s.maxPoolCapacity), - Path: s.dataFlow, - Fragment: s.tlsCode, - } - - // 发送隧道配置信息 - _, err := s.tunnelTCPConn.Write(s.encode([]byte(tunnelURL.String()))) - if err != nil { - return fmt.Errorf("tunnelHandshake: write tunnel config failed: %w", err) - } - - s.logger.Info("Tunnel signal -> : %v -> %v", tunnelURL.String(), s.tunnelTCPConn.RemoteAddr()) - s.logger.Info("Tunnel handshaked: %v <-> %v", s.tunnelTCPConn.LocalAddr(), s.tunnelTCPConn.RemoteAddr()) return nil } + +// tunnelHandshake 与客户端进行HTTP握手 +func (s *Server) tunnelHandshake() error { + var clientIP string + done := make(chan struct{}) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "close") + + // 验证请求 + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + // 验证路径 + if r.URL.Path != "/" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + // 验证令牌 + auth := r.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Bearer ") || !s.verifyAuthToken(strings.TrimPrefix(auth, "Bearer ")) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // 记录客户端地址 + clientIP = r.RemoteAddr + if host, _, err := net.SplitHostPort(clientIP); err == nil { + clientIP = host + } + + // 发送配置 + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "flow": s.dataFlow, + "max": s.maxPoolCapacity, + "tls": s.tlsCode, + "type": s.poolType, + }) + + s.logger.Info("Sending tunnel config: FLOW=%v|MAX=%v|TLS=%v|TYPE=%v", + s.dataFlow, s.maxPoolCapacity, s.tlsCode, s.poolType) + + close(done) + }) + + server := &http.Server{Handler: handler} + go server.Serve(s.tunnelListener) + + select { + case <-done: + server.Close() + s.clientIP = clientIP + s.tunnelListener, _ = net.ListenTCP("tcp", s.tunnelTCPAddr) + return nil + case <-s.ctx.Done(): + server.Close() + return fmt.Errorf("tunnelHandshake: context canceled") + } +} diff --git a/openwrt-passwall/luci-app-passwall/Makefile b/openwrt-passwall/luci-app-passwall/Makefile index 02ecc563a7..f26e344a8a 100644 --- a/openwrt-passwall/luci-app-passwall/Makefile +++ b/openwrt-passwall/luci-app-passwall/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=25.12.16 +PKG_VERSION:=25.12.19 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua b/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua index 6b5ed1211f..7f373a9c72 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/controller/passwall.lua @@ -502,15 +502,11 @@ function delete_select_nodes() local ids = http.formvalue("ids") local redirect = http.formvalue("redirect") string.gsub(ids, '[^' .. "," .. ']+', function(w) - if (uci:get(appname, "@global[0]", "tcp_node") or "") == w then - uci:delete(appname, '@global[0]', "tcp_node") - end - if (uci:get(appname, "@global[0]", "udp_node") or "") == w then - uci:delete(appname, '@global[0]', "udp_node") - end + local socks uci:foreach(appname, "socks", function(t) if t["node"] == w then uci:delete(appname, t[".name"]) + socks = "Socks_" .. t[".name"] end local auto_switch_node_list = uci:get(appname, t[".name"], "autoswitch_backup_node") or {} for i = #auto_switch_node_list, 1, -1 do @@ -520,16 +516,24 @@ function delete_select_nodes() end uci:set_list(appname, t[".name"], "autoswitch_backup_node", auto_switch_node_list) end) + local tcp_node = uci:get(appname, "@global[0]", "tcp_node") or "" + if tcp_node == w or tcp_node == socks then + uci:delete(appname, '@global[0]', "tcp_node") + end + local udp_node = uci:get(appname, "@global[0]", "udp_node") or "" + if udp_node == w or udp_node == socks then + uci:delete(appname, '@global[0]', "udp_node") + end uci:foreach(appname, "haproxy_config", function(t) if t["lbss"] == w then uci:delete(appname, t[".name"]) end end) uci:foreach(appname, "acl_rule", function(t) - if t["tcp_node"] == w then + if t["tcp_node"] == w or t["tcp_node"] == socks then uci:delete(appname, t[".name"], "tcp_node") end - if t["udp_node"] == w then + if t["udp_node"] == w or t["udp_node"] == socks then uci:delete(appname, t[".name"], "udp_node") end end) @@ -549,7 +553,7 @@ function delete_select_nodes() local changed = false local new_nodes = {} for _, node in ipairs(nodes) do - if node ~= w then + if node ~= w and node ~= socks then table.insert(new_nodes, node) else changed = true @@ -560,7 +564,7 @@ function delete_select_nodes() end end end - if t["fallback_node"] == w then + if t["fallback_node"] == w or t["fallback_node"] == socks then uci:delete(appname, t[".name"], "fallback_node") end end) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index e822fb6d65..ed8796f350 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -35,6 +35,17 @@ for _, v in pairs(nodes_table) do end end +local socks_list = {} +m.uci:foreach(appname, "socks", function(s) + if s.enabled == "1" and s.node then + socks_list[#socks_list + 1] = { + id = "Socks_" .. s[".name"], + remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")), + group = "Socks" + } + end +end) + local dynamicList_write = function(self, section, value) local t = {} local t2 = {} @@ -374,9 +385,7 @@ o:value("tcp", "TCP") o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")") o:depends("dns_mode", "xray") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, ["tcp+doh"] = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "xray" then @@ -391,9 +400,7 @@ o:value("tcp", "TCP") o:value("doh", "DoH") o:depends("dns_mode", "sing-box") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, doh = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "sing-box" then @@ -489,6 +496,12 @@ o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"} local tcp = s.fields["tcp_node"] local udp = s.fields["udp_node"] +for k, v in pairs(socks_list) do + tcp:value(v.id, v["remark"]) + tcp.group[#tcp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + udp:value(v.id, v["remark"]) + udp.group[#udp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end for k, v in pairs(nodes_table) do if #normal_list == 0 then s.fields["dns_mode"]:depends({ _tcp_node_bool = "1" }) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index de37538e08..68d884f470 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -480,9 +480,7 @@ o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")") o:depends("dns_mode", "xray") o:depends("smartdns_dns_mode", "xray") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, ["tcp+doh"] = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "xray" or s.fields["smartdns_dns_mode"]:formvalue(section) == "xray" then @@ -498,9 +496,7 @@ o:value("doh", "DoH") o:depends("dns_mode", "sing-box") o:depends("smartdns_dns_mode", "sing-box") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, doh = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "sing-box" or s.fields["smartdns_dns_mode"]:formvalue(section) == "sing-box" then @@ -767,6 +763,52 @@ function s2.create(e, t) TypedSection.create(e, t) luci.http.redirect(e.extedit:format(t)) end +function s2.remove(e, t) + local socks = "Socks_" .. t + local new_node = "" + local node0 = m:get("@nodes[0]") or nil + if node0 then + new_node = node0[".name"] + end + if (m:get("@global[0]", "tcp_node") or "") == socks then + m:set('@global[0]', "tcp_node", new_node) + end + if (m:get("@global[0]", "udp_node") or "") == socks then + m:set('@global[0]', "udp_node", new_node) + end + m.uci:foreach(appname, "acl_rule", function(s) + if s["tcp_node"] and s["tcp_node"] == socks then + m:set(s[".name"], "tcp_node", "default") + end + if s["udp_node"] and s["udp_node"] == socks then + m:set(s[".name"], "udp_node", "default") + end + end) + m.uci:foreach(appname, "nodes", function(s) + local list_name = s["urltest_node"] and "urltest_node" or (s["balancing_node"] and "balancing_node") + if list_name then + local nodes = m.uci:get_list(appname, s[".name"], list_name) + if nodes then + local changed = false + local new_nodes = {} + for _, node in ipairs(nodes) do + if node ~= socks then + table.insert(new_nodes, node) + else + changed = true + end + end + if changed then + m.uci:set_list(appname, s[".name"], list_name, new_nodes) + end + end + end + if s["fallback_node"] == socks then + m:del(s[".name"], "fallback_node") + end + end) + TypedSection.remove(e, t) +end o = s2:option(DummyValue, "status", translate("Status")) o.rawhtml = true @@ -817,6 +859,12 @@ end local tcp = s.fields["tcp_node"] local udp = s.fields["udp_node"] local socks = s2.fields["node"] +for k, v in pairs(socks_list) do + tcp:value(v.id, v["remark"]) + tcp.group[#tcp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + udp:value(v.id, v["remark"]) + udp.group[#udp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end for k, v in pairs(nodes_table) do if #normal_list == 0 then break diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua index 8c18638d3f..532783701d 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua @@ -108,6 +108,10 @@ o:depends({ [_n("protocol")] = "_balancing" }) o.widget = "checkbox" o.template = appname .. "/cbi/nodes_multivalue" o.group = {} +for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + o.group[#o.group+1] = v.group or "" +end for i, v in pairs(nodes_table) do o:value(v.id, v.remark) o.group[#o.group+1] = v.group or "" @@ -163,6 +167,10 @@ end if is_balancer then check_fallback_chain(arg[1]) end +for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end for k, v in pairs(fallback_table) do o:value(v.id, v.remark) o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index e918f1c9c2..070f755e03 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -114,6 +114,10 @@ o:depends({ [_n("protocol")] = "_urltest" }) o.widget = "checkbox" o.template = appname .. "/cbi/nodes_multivalue" o.group = {} +for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + o.group[#o.group+1] = v.group or "" +end for i, v in pairs(nodes_table) do o:value(v.id, v.remark) o.group[#o.group+1] = v.group or "" diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 8a81cdc063..b60dc9922a 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1061,12 +1061,30 @@ function gen_config(var) end end if is_new_ut_node then - local ut_node = uci:get_all(appname, ut_node_id) - local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) - if outbound then - outbound.tag = outbound.tag .. ":" .. ut_node.remarks - table.insert(outbounds, outbound) - valid_nodes[#valid_nodes + 1] = outbound.tag + local ut_node + if ut_node_id:find("Socks_") then + local socks_id = ut_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + ut_node = { + type = "sing-box", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + uot = "1", + remarks = "Socks_" .. socks_node.port + } + end + else + ut_node = uci:get_all(appname, ut_node_id) + end + if ut_node then + local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) + if outbound then + outbound.tag = outbound.tag .. ":" .. ut_node.remarks + table.insert(outbounds, outbound) + valid_nodes[#valid_nodes + 1] = outbound.tag + end end end end diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua index 2680514ef8..d18a0dd996 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -753,12 +753,31 @@ function gen_config(var) end end if is_new_blc_node then - local blc_node = uci:get_all(appname, blc_node_id) - local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) - if outbound then - outbound.tag = outbound.tag .. ":" .. blc_node.remarks - table.insert(outbounds, outbound) - valid_nodes[#valid_nodes + 1] = outbound.tag + local blc_node + if blc_node_id:find("Socks_") then + local socks_id = blc_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + blc_node = { + type = "Xray", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + transport = "tcp", + stream_security = "none", + remarks = "Socks_" .. socks_node.port + } + end + else + blc_node = uci:get_all(appname, blc_node_id) + end + if blc_node then + local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) + if outbound then + outbound.tag = outbound.tag .. ":" .. blc_node.remarks + table.insert(outbounds, outbound) + valid_nodes[#valid_nodes + 1] = outbound.tag + end end end end @@ -778,17 +797,36 @@ function gen_config(var) end end if is_new_node then - local fallback_node = uci:get_all(appname, fallback_node_id) - if fallback_node.protocol ~= "_balancing" then - local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) - if outbound then - outbound.tag = outbound.tag .. ":" .. fallback_node.remarks - table.insert(outbounds, outbound) - fallback_node_tag = outbound.tag + local fallback_node + if fallback_node_id:find("Socks_") then + local socks_id = fallback_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + fallback_node = { + type = "Xray", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + transport = "tcp", + stream_security = "none", + remarks = "Socks_" .. socks_node.port + } end else - if gen_balancer(fallback_node) then - fallback_node_tag = fallback_node_id + fallback_node = uci:get_all(appname, fallback_node_id) + end + if fallback_node then + if fallback_node.protocol ~= "_balancing" then + local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) + if outbound then + outbound.tag = outbound.tag .. ":" .. fallback_node.remarks + table.insert(outbounds, outbound) + fallback_node_tag = outbound.tag + end + else + if gen_balancer(fallback_node) then + fallback_node_tag = fallback_node_id + end end end end diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh index 0bd0c06870..9701240f91 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh @@ -276,6 +276,13 @@ eval_unset_val() { done } +is_socks_wrap() { + case "$1" in + Socks_*) return 0 ;; + *) return 1 ;; + esac +} + ln_run() { local file_func=${1} local ln_name=${2} @@ -633,10 +640,24 @@ run_socks() { else log_file="/dev/null" fi - local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') - local remarks=$(config_n_get $node remarks) - local server_host=$(config_n_get $node address) - local server_port=$(config_n_get $node port) + + local node2socks_port=0 + local type remarks server_host server_port + if is_socks_wrap "$node"; then + node2socks_port=$(config_n_get ${node#Socks_} port 0) + fi + if [ "$node2socks_port" = "0" ]; then + type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + remarks=$(config_n_get $node remarks) + server_host=$(config_n_get $node address) + server_port=$(config_n_get $node port) + else + type="socks" + server_host="127.0.0.1" + server_port=$node2socks_port + remarks="Socks 配置($server_port 端口)" + fi + [ -n "$relay_port" ] && { server_host="127.0.0.1" server_port=$relay_port @@ -669,10 +690,16 @@ run_socks() { case "$type" in socks) - local _socks_address=$(config_n_get $node address) - local _socks_port=$(config_n_get $node port) - local _socks_username=$(config_n_get $node username) - local _socks_password=$(config_n_get $node password) + local _socks_address _socks_port _socks_username _socks_password + if [ "$node2socks_port" = "0" ]; then + _socks_address=$(config_n_get $node address) + _socks_port=$(config_n_get $node port) + _socks_username=$(config_n_get $node username) + _socks_password=$(config_n_get $node password) + else + _socks_address="127.0.0.1" + _socks_port=$node2socks_port + fi [ "$http_port" != "0" ] && { http_flag=1 config_file="${config_file//SOCKS/HTTP_SOCKS}" @@ -795,12 +822,26 @@ run_redir() { fi local proto=$(echo $proto | tr 'A-Z' 'a-z') local PROTO=$(echo $proto | tr 'a-z' 'A-Z') - local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + + local node2socks_port=0 + local type remarks server_host port + if is_socks_wrap "$node"; then + node2socks_port=$(config_n_get ${node#Socks_} port 0) + fi + if [ "$node2socks_port" = "0" ]; then + type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + remarks=$(config_n_get $node remarks) + server_host=$(config_n_get $node address) + port=$(config_n_get $node port) + else + type="socks" + server_host="127.0.0.1" + port=$node2socks_port + remarks="Socks 配置($port 端口)" + fi + local enable_log=$(config_t_get global log_${proto} 1) [ "$enable_log" != "1" ] && log_file="/dev/null" - local remarks=$(config_n_get $node remarks) - local server_host=$(config_n_get $node address) - local port=$(config_n_get $node port) [ -n "$server_host" ] && [ -n "$port" ] && { check_host $server_host [ $? != 0 ] && { @@ -814,10 +855,16 @@ run_redir() { UDP) case "$type" in socks) - local _socks_address=$(config_n_get $node address) - local _socks_port=$(config_n_get $node port) - local _socks_username=$(config_n_get $node username) - local _socks_password=$(config_n_get $node password) + local _socks_address _socks_port _socks_username _socks_password + if [ "$node2socks_port" = "0" ]; then + _socks_address=$(config_n_get $node address) + _socks_port=$(config_n_get $node port) + _socks_username=$(config_n_get $node username) + _socks_password=$(config_n_get $node password) + else + _socks_address="127.0.0.1" + _socks_port=$node2socks_port + fi run_ipt2socks flag=default proto=UDP local_port=${local_port} socks_address=${_socks_address} socks_port=${_socks_port} socks_username=${_socks_username} socks_password=${_socks_password} log_file=${log_file} ;; sing-box) @@ -898,10 +945,15 @@ run_redir() { case "$type" in socks) _socks_flag=1 - _socks_address=$(config_n_get $node address) - _socks_port=$(config_n_get $node port) - _socks_username=$(config_n_get $node username) - _socks_password=$(config_n_get $node password) + if [ "$node2socks_port" = "0" ]; then + _socks_address=$(config_n_get $node address) + _socks_port=$(config_n_get $node port) + _socks_username=$(config_n_get $node username) + _socks_password=$(config_n_get $node password) + else + _socks_address="127.0.0.1" + _socks_port=$node2socks_port + fi [ -z "$can_ipt" ] && { local _config_file=$config_file _config_file="TCP_SOCKS_${node}.json" @@ -1856,7 +1908,7 @@ acl_app() { echolog " - 全局节点未启用,跳过【${remarks}】" fi else - [ "$(config_get_type $tcp_node)" = "nodes" ] && { + [ "$(config_get_type $tcp_node)" = "nodes" ] || [ "$(config_get_type ${tcp_node#Socks_})" = "socks" ] && { if [ -n "${GLOBAL_TCP_NODE}" ] && [ "$tcp_node" = "${GLOBAL_TCP_NODE}" ]; then set_cache_var "ACL_${sid}_tcp_node" "${GLOBAL_TCP_NODE}" set_cache_var "ACL_${sid}_tcp_redir_port" "${GLOBAL_TCP_redir_port}" @@ -2015,7 +2067,7 @@ acl_app() { set_cache_var "ACL_${sid}_udp_node" "${udp_node}" set_cache_var "ACL_${sid}_udp_redir_port" "${udp_port}" else - [ "$(config_get_type $udp_node)" = "nodes" ] && { + [ "$(config_get_type $udp_node)" = "nodes" ] || [ "$(config_get_type ${udp_node#Socks_})" = "socks" ] && { if [ -n "${GLOBAL_UDP_NODE}" ] && [ "$udp_node" = "${GLOBAL_UDP_NODE}" ]; then set_cache_var "ACL_${sid}_udp_node" "${GLOBAL_UDP_NODE}" set_cache_var "ACL_${sid}_udp_redir_port" "${GLOBAL_UDP_redir_port}" @@ -2189,18 +2241,23 @@ get_config() { TCP_NODE=$(config_t_get global tcp_node) UDP_NODE=$(config_t_get global udp_node) TCP_UDP=0 - if [ "$UDP_NODE" == "tcp" ]; then + if [ "$UDP_NODE" = "tcp" ]; then UDP_NODE=$TCP_NODE TCP_UDP=1 - elif [ "$UDP_NODE" == "$TCP_NODE" ]; then + elif [ "$UDP_NODE" = "$TCP_NODE" ]; then TCP_UDP=1 fi - [ "$ENABLED" == 1 ] && { - [ -n "$TCP_NODE" ] && [ "$(config_get_type $TCP_NODE)" == "nodes" ] && ENABLED_DEFAULT_ACL=1 - [ -n "$UDP_NODE" ] && [ "$(config_get_type $UDP_NODE)" == "nodes" ] && ENABLED_DEFAULT_ACL=1 + [ "$ENABLED" = 1 ] && { + local _node + for _node in "$TCP_NODE" "$UDP_NODE"; do + [ -n "$_node" ] && case "$_node" in + Socks_*) [ "$(config_get_type "${_node#Socks_}")" = "socks" ] && ENABLED_DEFAULT_ACL=1 ;; + *) [ "$(config_get_type "$_node")" = "nodes" ] && ENABLED_DEFAULT_ACL=1 ;; + esac + done } ENABLED_ACLS=$(config_t_get global acl_enable 0) - [ "$ENABLED_ACLS" == 1 ] && { + [ "$ENABLED_ACLS" = 1 ] && { [ "$(uci show ${CONFIG} | grep "@acl_rule" | grep "enabled='1'" | wc -l)" == 0 ] && ENABLED_ACLS=0 } diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh index f3397f0e31..90775baa12 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -256,8 +256,20 @@ load_acl() { [ -n "$(get_cache_var "ACL_${sid}_udp_node")" ] && udp_node=$(get_cache_var "ACL_${sid}_udp_node") [ -n "$(get_cache_var "ACL_${sid}_udp_redir_port")" ] && udp_port=$(get_cache_var "ACL_${sid}_udp_redir_port") [ -n "$(get_cache_var "ACL_${sid}_dns_port")" ] && dns_redirect_port=$(get_cache_var "ACL_${sid}_dns_port") - [ -n "$tcp_node" ] && tcp_node_remark=$(config_n_get $tcp_node remarks) - [ -n "$udp_node" ] && udp_node_remark=$(config_n_get $udp_node remarks) + [ -n "$tcp_node" ] && { + if is_socks_wrap "$tcp_node"; then + tcp_node_remark="Socks 配置($(config_n_get ${tcp_node#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $tcp_node remarks) + fi + } + [ -n "$udp_node" ] && { + if is_socks_wrap "$udp_node"; then + udp_node_remark="Socks 配置($(config_n_get ${udp_node#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $udp_node remarks) + fi + } use_shunt_tcp=0 use_shunt_udp=0 @@ -265,8 +277,16 @@ load_acl() { [ -n "$udp_node" ] && [ "$(config_n_get $udp_node protocol)" = "_shunt" ] && use_shunt_udp=1 [ "${use_global_config}" = "1" ] && { - tcp_node_remark=$(config_n_get $TCP_NODE remarks) - udp_node_remark=$(config_n_get $UDP_NODE remarks) + if is_socks_wrap "$TCP_NODE"; then + tcp_node_remark="Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $TCP_NODE remarks) + fi + if is_socks_wrap "$UDP_NODE"; then + udp_node_remark="Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $UDP_NODE remarks) + fi use_direct_list=${USE_DIRECT_LIST} use_proxy_list=${USE_PROXY_LIST} use_block_list=${USE_BLOCK_LIST} @@ -638,7 +658,11 @@ load_acl() { # 加载TCP默认代理模式 if [ -n "${TCP_PROXY_MODE}" ]; then [ -n "$TCP_NODE" ] && { - msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + if is_socks_wrap "$TCP_NODE"; then + msg2="${msg}使用 TCP 节点[Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)]" + else + msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + fi if [ -n "${is_tproxy}" ]; then msg2="${msg2}(TPROXY:${TCP_REDIR_PORT})" ipt_j="-j PSW_RULE" @@ -693,7 +717,11 @@ load_acl() { # 加载UDP默认代理模式 if [ -n "${UDP_PROXY_MODE}" ]; then [ -n "$UDP_NODE" -o "$TCP_UDP" = "1" ] && { - msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + if is_socks_wrap "$UDP_NODE"; then + msg2="${msg}使用 UDP 节点[Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)](TPROXY:${UDP_REDIR_PORT})" + else + msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + fi $ipt_m -A PSW $(comment "默认") -p udp -d $FAKE_IP -j PSW_RULE [ "${USE_PROXY_LIST}" = "1" ] && add_port_rules "$ipt_m -A PSW $(comment "默认") -p udp" $UDP_REDIR_PORTS "$(dst $IPSET_BLACK) -j PSW_RULE" diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh index 99c4a8e7b6..8036d9601f 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -281,17 +281,36 @@ load_acl() { [ -n "$(get_cache_var "ACL_${sid}_udp_node")" ] && udp_node=$(get_cache_var "ACL_${sid}_udp_node") [ -n "$(get_cache_var "ACL_${sid}_udp_redir_port")" ] && udp_port=$(get_cache_var "ACL_${sid}_udp_redir_port") [ -n "$(get_cache_var "ACL_${sid}_dns_port")" ] && dns_redirect_port=$(get_cache_var "ACL_${sid}_dns_port") - [ -n "$tcp_node" ] && tcp_node_remark=$(config_n_get $tcp_node remarks) - [ -n "$udp_node" ] && udp_node_remark=$(config_n_get $udp_node remarks) - + [ -n "$tcp_node" ] && { + if is_socks_wrap "$tcp_node"; then + tcp_node_remark="Socks 配置($(config_n_get ${tcp_node#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $tcp_node remarks) + fi + } + [ -n "$udp_node" ] && { + if is_socks_wrap "$udp_node"; then + udp_node_remark="Socks 配置($(config_n_get ${udp_node#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $udp_node remarks) + fi + } use_shunt_tcp=0 use_shunt_udp=0 [ -n "$tcp_node" ] && [ "$(config_n_get $tcp_node protocol)" = "_shunt" ] && use_shunt_tcp=1 [ -n "$udp_node" ] && [ "$(config_n_get $udp_node protocol)" = "_shunt" ] && use_shunt_udp=1 [ "${use_global_config}" = "1" ] && { - tcp_node_remark=$(config_n_get $TCP_NODE remarks) - udp_node_remark=$(config_n_get $UDP_NODE remarks) + if is_socks_wrap "$TCP_NODE"; then + tcp_node_remark="Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $TCP_NODE remarks) + fi + if is_socks_wrap "$UDP_NODE"; then + udp_node_remark="Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $UDP_NODE remarks) + fi use_direct_list=${USE_DIRECT_LIST} use_proxy_list=${USE_PROXY_LIST} use_block_list=${USE_BLOCK_LIST} @@ -660,7 +679,11 @@ load_acl() { # 加载TCP默认代理模式 if [ -n "${TCP_PROXY_MODE}" ]; then [ -n "$TCP_NODE" ] && { - msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + if is_socks_wrap "$TCP_NODE"; then + msg2="${msg}使用 TCP 节点[Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)]" + else + msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + fi if [ -n "${is_tproxy}" ]; then msg2="${msg2}(TPROXY:${TCP_REDIR_PORT})" nft_chain="PSW_MANGLE" @@ -720,7 +743,11 @@ load_acl() { # 加载UDP默认代理模式 if [ -n "${UDP_PROXY_MODE}" ]; then [ -n "$UDP_NODE" -o "$TCP_UDP" = "1" ] && { - msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + if is_socks_wrap "$UDP_NODE"; then + msg2="${msg}使用 UDP 节点[Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)](TPROXY:${UDP_REDIR_PORT})" + else + msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + fi nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW_RULE comment \"默认\"" [ "${USE_PROXY_LIST}" = "1" ] && nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") ip daddr @$NFTSET_BLACK counter jump PSW_RULE comment \"默认\"" diff --git a/shadowsocks-rust/Cargo.lock b/shadowsocks-rust/Cargo.lock index f97cf9cbdc..fd107cd8d0 100644 --- a/shadowsocks-rust/Cargo.lock +++ b/shadowsocks-rust/Cargo.lock @@ -3120,6 +3120,7 @@ name = "shadowsocks-service" version = "1.24.0" dependencies = [ "arc-swap", + "base64", "brotli", "bson", "byte_string", @@ -3667,9 +3668,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3702,9 +3703,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", diff --git a/shadowsocks-rust/README.md b/shadowsocks-rust/README.md index 5ef8923033..11cb2f9b15 100644 --- a/shadowsocks-rust/README.md +++ b/shadowsocks-rust/README.md @@ -614,7 +614,10 @@ Example configuration: "local_address": "127.0.0.1", "local_port": 3128, // OPTIONAL. macOS launchd activate socket - "launchd_tcp_socket_name": "TCPListener" + "launchd_tcp_socket_name": "TCPListener", + // OPTIONAL. Authentication configuration file + // Configuration file document could be found in the next section. + "http_auth_config_path": "/path/to/auth.json", }, { // DNS local server (feature = "local-dns") @@ -955,6 +958,24 @@ The configuration file is set by `socks5_auth_config_path` in `locals`. } ``` +### HTTP Authentication Configuration + +The configuration file is set by `http_auth_config_path` in `locals`. + +```jsonc +{ + // Basic Authentication (RFC9110) + "basic": { + "users": [ + { + "user_name": "USERNAME in UTF-8", + "password": "PASSWORD in UTF-8" + } + ] + } +} +``` + ### Environment Variables - `SS_SERVER_PASSWORD`: A default password for servers that created from command line argument (`--server-addr`) diff --git a/shadowsocks-rust/bin/ssservice.rs b/shadowsocks-rust/bin/ssservice.rs index 7e0b3247c0..e8e5b17262 100644 --- a/shadowsocks-rust/bin/ssservice.rs +++ b/shadowsocks-rust/bin/ssservice.rs @@ -19,14 +19,15 @@ fn main() -> ExitCode { // Allow running `ssservice` as symlink of `sslocal`, `ssserver` and `ssmanager` if let Some(program_path) = env::args().next() - && let Some(program_name) = Path::new(&program_path).file_name() { - match program_name.to_str() { - Some("sslocal") => return local::main(&local::define_command_line_options(app).get_matches()), - Some("ssserver") => return server::main(&server::define_command_line_options(app).get_matches()), - Some("ssmanager") => return manager::main(&manager::define_command_line_options(app).get_matches()), - _ => {} - } + && let Some(program_name) = Path::new(&program_path).file_name() + { + match program_name.to_str() { + Some("sslocal") => return local::main(&local::define_command_line_options(app).get_matches()), + Some("ssserver") => return server::main(&server::define_command_line_options(app).get_matches()), + Some("ssmanager") => return manager::main(&manager::define_command_line_options(app).get_matches()), + _ => {} } + } let matches = app .subcommand_required(true) diff --git a/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml b/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml index 2a78e532b1..bfe5d2357e 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml +++ b/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml @@ -60,7 +60,7 @@ local-dns-relay = ["local-dns"] # Currently is only used in Android local-flow-stat = ["local"] # Enable HTTP protocol for sslocal -local-http = ["local", "hyper", "http", "http-body-util"] +local-http = ["local", "hyper", "http", "http-body-util", "base64"] local-http-native-tls = ["local-http", "tokio-native-tls", "native-tls"] local-http-native-tls-vendored = [ "local-http-native-tls", @@ -179,6 +179,7 @@ mime = { version = "0.3", optional = true } flate2 = { version = "1.0", optional = true } brotli = { version = "8.0", optional = true } zstd = { version = "0.13", optional = true } +base64 = { version = "0.22", optional = true } etherparse = { version = "0.19", optional = true } smoltcp = { version = "0.12", optional = true, default-features = false, features = [ diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/acl/mod.rs b/shadowsocks-rust/crates/shadowsocks-service/src/acl/mod.rs index c8e1e4ac6b..494bbbd6ae 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/acl/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/acl/mod.rs @@ -209,13 +209,14 @@ impl ParsingRules { } } } else if let Some(set_rule) = caps.get(2) - && let Ok(set_rule) = str::from_utf8(set_rule.as_bytes()) { - let set_rule = set_rule.replace("\\.", "."); - if self.add_set_rule_inner(&set_rule).is_ok() { - trace!("REGEX-RULE {} => SET-RULE {}", rule, set_rule); - return; - } + && let Ok(set_rule) = str::from_utf8(set_rule.as_bytes()) + { + let set_rule = set_rule.replace("\\.", "."); + if self.add_set_rule_inner(&set_rule).is_ok() { + trace!("REGEX-RULE {} => SET-RULE {}", rule, set_rule); + return; } + } } trace!("REGEX-RULE {}", rule); diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/config.rs b/shadowsocks-rust/crates/shadowsocks-service/src/config.rs index a449e94226..ef48112eaf 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/config.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/config.rs @@ -80,6 +80,8 @@ use shadowsocks::{ use crate::acl::AccessControl; #[cfg(feature = "local-dns")] use crate::local::dns::NameServerAddr; +#[cfg(feature = "local-http")] +use crate::local::http::config::HttpAuthConfig; #[cfg(feature = "local")] use crate::local::socks::config::Socks5AuthConfig; @@ -323,6 +325,11 @@ struct SSLocalExtConfig { #[serde(skip_serializing_if = "Option::is_none")] socks5_auth_config_path: Option, + /// HTTP + #[cfg(feature = "local-http")] + #[serde(skip_serializing_if = "Option::is_none")] + http_auth_config_path: Option, + /// Fake DNS #[cfg(feature = "local-fake-dns")] #[serde(skip_serializing_if = "Option::is_none")] @@ -1038,6 +1045,10 @@ pub struct LocalConfig { #[cfg(feature = "local")] pub socks5_auth: Socks5AuthConfig, + /// HTTP Authentication configuration + #[cfg(feature = "local-http")] + pub http_auth: HttpAuthConfig, + /// Fake DNS record expire seconds #[cfg(feature = "local-fake-dns")] pub fake_dns_record_expire_duration: Option, @@ -1108,6 +1119,9 @@ impl LocalConfig { #[cfg(feature = "local")] socks5_auth: Socks5AuthConfig::default(), + #[cfg(feature = "local-http")] + http_auth: HttpAuthConfig::default(), + #[cfg(feature = "local-fake-dns")] fake_dns_record_expire_duration: None, #[cfg(feature = "local-fake-dns")] @@ -1832,6 +1846,11 @@ impl Config { local_config.socks5_auth = Socks5AuthConfig::load_from_file(&socks5_auth_config_path)?; } + #[cfg(feature = "local-http")] + if let Some(http_auth_config_path) = local.http_auth_config_path { + local_config.http_auth = HttpAuthConfig::load_from_file(&http_auth_config_path)?; + } + #[cfg(feature = "local-fake-dns")] { if let Some(d) = local.fake_dns_record_expire_duration { @@ -2390,15 +2409,16 @@ impl Config { // Security if let Some(sec) = config.security && let Some(replay_attack) = sec.replay_attack - && let Some(policy) = replay_attack.policy { - match policy.parse::() { - Ok(p) => nconfig.security.replay_attack.policy = p, - Err(..) => { - let err = Error::new(ErrorKind::Invalid, "invalid replay attack policy", None); - return Err(err); - } - } + && let Some(policy) = replay_attack.policy + { + match policy.parse::() { + Ok(p) => nconfig.security.replay_attack.policy = p, + Err(..) => { + let err = Error::new(ErrorKind::Invalid, "invalid replay attack policy", None); + return Err(err); } + } + } if let Some(balancer) = config.balancer { nconfig.balancer = BalancerConfig { @@ -2619,16 +2639,18 @@ impl Config { // Balancer related checks if let Some(rtt) = self.balancer.max_server_rtt - && rtt.as_secs() == 0 { - let err = Error::new(ErrorKind::Invalid, "balancer.max_server_rtt must be > 0", None); - return Err(err); - } + && rtt.as_secs() == 0 + { + let err = Error::new(ErrorKind::Invalid, "balancer.max_server_rtt must be > 0", None); + return Err(err); + } if let Some(intv) = self.balancer.check_interval - && intv.as_secs() == 0 { - let err = Error::new(ErrorKind::Invalid, "balancer.check_interval must be > 0", None); - return Err(err); - } + && intv.as_secs() == 0 + { + let err = Error::new(ErrorKind::Invalid, "balancer.check_interval must be > 0", None); + return Err(err); + } } if self.config_type.is_server() && self.server.is_empty() { @@ -2664,10 +2686,11 @@ impl Config { // Plugin shouldn't be an empty string if let Some(plugin) = server.plugin() - && plugin.plugin.trim().is_empty() { - let err = Error::new(ErrorKind::Malformed, "`plugin` shouldn't be an empty string", None); - return Err(err); - } + && plugin.plugin.trim().is_empty() + { + let err = Error::new(ErrorKind::Malformed, "`plugin` shouldn't be an empty string", None); + return Err(err); + } // Server's domain name shouldn't be an empty string match server.addr() { @@ -2899,6 +2922,9 @@ impl fmt::Display for Config { #[cfg(feature = "local")] socks5_auth_config_path: None, + #[cfg(feature = "local-http")] + http_auth_config_path: None, + #[cfg(feature = "local-fake-dns")] fake_dns_record_expire_duration: local.fake_dns_record_expire_duration.map(|d| d.as_secs()), #[cfg(feature = "local-fake-dns")] @@ -3068,20 +3094,22 @@ impl fmt::Display for Config { } if jconf.method.is_none() - && let Some(ref m) = m.method { - jconf.method = Some(m.to_string()); - } + && let Some(ref m) = m.method + { + jconf.method = Some(m.to_string()); + } if jconf.plugin.is_none() - && let Some(ref p) = m.plugin { - jconf.plugin = Some(p.plugin.clone()); - if let Some(ref o) = p.plugin_opts { - jconf.plugin_opts = Some(o.clone()); - } - if !p.plugin_args.is_empty() { - jconf.plugin_args = Some(p.plugin_args.clone()); - } + && let Some(ref p) = m.plugin + { + jconf.plugin = Some(p.plugin.clone()); + if let Some(ref o) = p.plugin_opts { + jconf.plugin_opts = Some(o.clone()); } + if !p.plugin_args.is_empty() { + jconf.plugin_args = Some(p.plugin_args.clone()); + } + } } if self.no_delay { @@ -3188,17 +3216,18 @@ impl fmt::Display for Config { /// It will return the original value if fails to read `${VAR_NAME}`. pub fn read_variable_field_value(value: &str) -> Cow<'_, str> { if let Some(left_over) = value.strip_prefix("${") - && let Some(var_name) = left_over.strip_suffix('}') { - match env::var(var_name) { - Ok(value) => return value.into(), - Err(err) => { - warn!( - "couldn't read password from environment variable {}, error: {}", - var_name, err - ); - } + && let Some(var_name) = left_over.strip_suffix('}') + { + match env::var(var_name) { + Ok(value) => return value.into(), + Err(err) => { + warn!( + "couldn't read password from environment variable {}, error: {}", + var_name, err + ); } } + } value.into() } diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/config.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/config.rs new file mode 100644 index 0000000000..8ff1e5c0bf --- /dev/null +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/config.rs @@ -0,0 +1,172 @@ +//! HTTP protocol configuration + +use std::{ + collections::HashMap, + fs::OpenOptions, + io::{self, Read}, + path::Path, +}; + +use base64::Engine as _; +use log::trace; +use serde::Deserialize; + +const BASIC_AUTH_BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new() + .with_encode_padding(true) + .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent), +); + +#[derive(Deserialize, Debug)] +struct SSHttpAuthBasicUserConfig { + user_name: String, + password: String, +} + +#[derive(Deserialize, Debug)] +struct SSHttpAuthBasicConfig { + users: Vec, +} + +#[derive(Deserialize, Debug)] +struct SSHttpAuthConfig { + #[serde(skip_serializing_if = "Option::is_none")] + basic: Option, +} + +/// HTTP Authentication method +#[derive(Debug, Clone)] +pub struct HttpAuthConfig { + pub basic: HttpAuthBasicConfig, +} + +impl HttpAuthConfig { + /// Create a new HTTP Authentication configuration + pub fn new() -> Self { + Self { + basic: HttpAuthBasicConfig::new(), + } + } + + /// Load from configuration file + /// + /// ```json + /// { + /// "basic": { + /// "users": [ + /// { + /// "user_name": "USER_NAME", + /// "password": "PASSWORD" + /// } + /// ] + /// } + /// } + pub fn load_from_file + ?Sized>(filename: &P) -> io::Result { + let filename = filename.as_ref(); + + trace!( + "loading socks5 authentication configuration from {}", + filename.display() + ); + + let mut reader = OpenOptions::new().read(true).open(filename)?; + let mut content = String::new(); + reader.read_to_string(&mut content)?; + + let jconf: SSHttpAuthConfig = match json5::from_str(&content) { + Ok(c) => c, + Err(err) => return Err(io::Error::other(err)), + }; + + let mut basic = HttpAuthBasicConfig::new(); + if let Some(p) = jconf.basic { + for user in p.users { + basic.add_user(user.user_name, user.password); + } + } + + Ok(Self { basic }) + } + + /// Check if authentication is required + pub fn auth_required(&self) -> bool { + self.basic.total_users() > 0 + } + + /// Check Basic Authentication + pub fn verify_basic_auth(&self, header_value: &str) -> bool { + const BASIC_PREFIX: &str = "Basic "; + + if !header_value.starts_with(BASIC_PREFIX) { + return false; + } + let b64_encoded = &header_value[BASIC_PREFIX.len()..]; + let decoded_bytes = match BASIC_AUTH_BASE64_ENGINE.decode(b64_encoded) { + Ok(b) => b, + Err(_) => return false, + }; + let decoded_str = match String::from_utf8(decoded_bytes) { + Ok(s) => s, + Err(_) => return false, + }; + let (user_name, password) = match decoded_str.split_once(':') { + Some((u, p)) => (u, p), + None => return false, + }; + self.basic.check_user(user_name, password) + } +} + +impl Default for HttpAuthConfig { + fn default() -> Self { + Self::new() + } +} + +/// HTTP server User/Password Authentication configuration +/// +/// RFC9110 https://httpwg.org/specs/rfc9110.html#auth.client.proxy +#[derive(Debug, Clone)] +pub struct HttpAuthBasicConfig { + users: HashMap, +} + +impl HttpAuthBasicConfig { + /// Create an empty `Passwd` configuration + pub fn new() -> Self { + Self { users: HashMap::new() } + } + + /// Add a user with password + pub fn add_user(&mut self, user_name: U, password: P) + where + U: Into, + P: Into, + { + self.users.insert(user_name.into(), password.into()); + } + + /// Check if `user_name` exists and validate `password` + pub fn check_user(&self, user_name: U, password: P) -> bool + where + U: AsRef, + P: AsRef, + { + match self.users.get(user_name.as_ref()) { + Some(pwd) => pwd == password.as_ref(), + None => false, + } + } + + /// Total users + pub fn total_users(&self) -> usize { + self.users.len() + } +} + +impl Default for HttpAuthBasicConfig { + fn default() -> Self { + Self::new() + } +} diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/http_service.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/http_service.rs index 441f2fe941..5901dfffc6 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/http_service.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/http_service.rs @@ -21,6 +21,7 @@ use crate::local::{ }; use super::{ + config::HttpAuthConfig, http_client::HttpClient, utils::{authority_addr, check_keep_alive, connect_host, host_addr}, }; @@ -30,6 +31,7 @@ pub struct HttpService { peer_addr: SocketAddr, http_client: HttpClient, balancer: PingBalancer, + http_auth: Arc, } impl HttpService { @@ -38,12 +40,14 @@ impl HttpService { peer_addr: SocketAddr, http_client: HttpClient, balancer: PingBalancer, + http_auth: Arc, ) -> Self { Self { context, peer_addr, http_client, balancer, + http_auth, } } @@ -53,6 +57,37 @@ impl HttpService { ) -> hyper::Result>> { trace!("request {} {:?}", self.peer_addr, req); + if self.http_auth.auth_required() { + // Only support Basic authentication for now + match req.headers().get(header::PROXY_AUTHORIZATION) { + Some(header_value) => { + if let Ok(header_str) = header_value.to_str() { + if !self.http_auth.verify_basic_auth(header_str) { + error!( + "HTTP {} URI {} invalid proxy-authorization: {}", + req.method(), + req.uri(), + header_str + ); + return make_proxy_authentication_required(); + } + } else { + error!( + "HTTP {} URI {} invalid proxy-authorization header encoding: {:?}", + req.method(), + req.uri(), + header_value + ); + return make_proxy_authentication_required(); + } + } + None => { + error!("HTTP {} URI {} missing proxy-authorization", req.method(), req.uri()); + return make_proxy_authentication_required(); + } + } + } + // Parse URI // // Proxy request URI must contains a host @@ -204,6 +239,14 @@ fn make_bad_request() -> Result>, hyper::E .unwrap()) } +fn make_proxy_authentication_required() -> Result>, hyper::Error> { + Ok(Response::builder() + .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED) + .header(header::PROXY_AUTHENTICATE, "Basic charset=\"UTF-8\"") + .body(empty_body()) + .unwrap()) +} + fn make_internal_server_error() -> Result>, hyper::Error> { Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/mod.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/mod.rs index 6228e0e04c..ee95faa351 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/mod.rs @@ -7,6 +7,7 @@ pub use self::{ server::{Http, HttpBuilder, HttpConnectionHandler}, }; +pub mod config; mod http_client; mod http_service; mod http_stream; diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/server.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/server.rs index 913e3b308e..a392246b14 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/http/server.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/http/server.rs @@ -16,7 +16,7 @@ use crate::local::{ context::ServiceContext, loadbalancing::PingBalancer, net::tcp::listener::create_standard_tcp_listener, }; -use super::{http_client::HttpClient, http_service::HttpService, tokio_rt::TokioIo}; +use super::{config::HttpAuthConfig, http_client::HttpClient, http_service::HttpService, tokio_rt::TokioIo}; /// HTTP Local server builder pub struct HttpBuilder { @@ -25,6 +25,7 @@ pub struct HttpBuilder { balancer: PingBalancer, #[cfg(target_os = "macos")] launchd_tcp_socket_name: Option, + http_auth: HttpAuthConfig, } impl HttpBuilder { @@ -42,6 +43,7 @@ impl HttpBuilder { balancer, #[cfg(target_os = "macos")] launchd_tcp_socket_name: None, + http_auth: HttpAuthConfig::default(), } } @@ -50,6 +52,10 @@ impl HttpBuilder { self.launchd_tcp_socket_name = Some(n); } + pub fn set_http_auth(&mut self, auth: HttpAuthConfig) { + self.http_auth = auth; + } + /// Build HTTP server instance pub async fn build(self) -> io::Result { cfg_if::cfg_if! { @@ -77,6 +83,7 @@ impl HttpBuilder { context: self.context, listener, balancer: self.balancer, + http_auth: Arc::new(self.http_auth), }) } } @@ -86,6 +93,7 @@ pub struct Http { context: Arc, listener: TcpListener, balancer: PingBalancer, + http_auth: Arc, } impl Http { @@ -104,7 +112,7 @@ impl Http { self.listener.local_addr().expect("http local_addr") ); - let handler = HttpConnectionHandler::new(self.context, self.balancer); + let handler = HttpConnectionHandler::new(self.context, self.balancer, self.http_auth); loop { let (stream, peer_addr) = match self.listener.accept().await { @@ -135,15 +143,17 @@ pub struct HttpConnectionHandler { context: Arc, balancer: PingBalancer, http_client: HttpClient, + http_auth: Arc, } impl HttpConnectionHandler { /// Create a new Handler - pub fn new(context: Arc, balancer: PingBalancer) -> Self { + pub fn new(context: Arc, balancer: PingBalancer, http_auth: Arc) -> Self { Self { context, balancer, http_client: HttpClient::new(), + http_auth, } } @@ -156,6 +166,7 @@ impl HttpConnectionHandler { context, balancer, http_client, + http_auth, } = self; let io = TokioIo::new(stream); @@ -169,8 +180,14 @@ impl HttpConnectionHandler { .serve_connection( io, service::service_fn(move |req| { - HttpService::new(context.clone(), peer_addr, http_client.clone(), balancer.clone()) - .serve_connection(req) + HttpService::new( + context.clone(), + peer_addr, + http_client.clone(), + balancer.clone(), + http_auth.clone(), + ) + .serve_connection(req) }), ) .with_upgrades() diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs index 7a538f9a1c..e5fa4b2482 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs @@ -164,9 +164,10 @@ impl PingBalancerBuilder { pub async fn build(self) -> io::Result { if let Some(intv) = self.check_best_interval - && intv > self.check_interval { - return Err(io::Error::other("check_interval must be >= check_best_interval")); - } + && intv > self.check_interval + { + return Err(io::Error::other("check_interval must be >= check_best_interval")); + } let (shared_context, task_abortable) = PingBalancerContext::new( self.servers, diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/mod.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/mod.rs index 753f72dd40..c234d065e6 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/mod.rs @@ -279,6 +279,8 @@ impl Server { let mut server_builder = SocksBuilder::with_context(context.clone(), client_addr, balancer); server_builder.set_mode(local_config.mode); server_builder.set_socks5_auth(local_config.socks5_auth); + #[cfg(feature = "local-http")] + server_builder.set_http_auth(local_config.http_auth); if let Some(c) = config.udp_max_associations { server_builder.set_udp_capacity(c); @@ -349,6 +351,7 @@ impl Server { #[allow(unused_mut)] let mut builder = HttpBuilder::with_context(context.clone(), client_addr, balancer); + builder.set_http_auth(local_config.http_auth); #[cfg(target_os = "macos")] if let Some(n) = local_config.launchd_tcp_socket_name { diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/net/udp/association.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/net/udp/association.rs index 066b0fb0eb..f3c753320d 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/net/udp/association.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/net/udp/association.rs @@ -515,11 +515,10 @@ where } }; - if UDP_SOCKET_SUPPORT_DUAL_STACK - && let SocketAddr::V4(saddr) = target_addr { - let mapped_ip = saddr.ip().to_ipv6_mapped(); - target_addr = SocketAddr::V6(SocketAddrV6::new(mapped_ip, saddr.port(), 0, 0)); - } + if UDP_SOCKET_SUPPORT_DUAL_STACK && let SocketAddr::V4(saddr) = target_addr { + let mapped_ip = saddr.ip().to_ipv6_mapped(); + target_addr = SocketAddr::V6(SocketAddrV6::new(mapped_ip, saddr.port(), 0, 0)); + } let n = socket.send_to(data, target_addr).await?; if n != data.len() { diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/online_config/mod.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/online_config/mod.rs index 277493f92e..9a4dd01f8f 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/online_config/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/online_config/mod.rs @@ -215,13 +215,14 @@ impl OnlineConfigService { if let Some(ref allowed_plugins) = self.allowed_plugins { for server in &online_config.server { if let Some(plugin) = server.config.plugin() - && !allowed_plugins.contains(&plugin.plugin) { - error!( - "server-loader task found not allowed plugin: {}, url: {}", - plugin.plugin, self.config_url - ); - return Err(io::Error::other(format!("not allowed plugin: {}", plugin.plugin))); - } + && !allowed_plugins.contains(&plugin.plugin) + { + error!( + "server-loader task found not allowed plugin: {}, url: {}", + plugin.plugin, self.config_url + ); + return Err(io::Error::other(format!("not allowed plugin: {}", plugin.plugin))); + } } } diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/tcprelay/mod.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/tcprelay/mod.rs index 75a95b8a24..bc8c19667f 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/tcprelay/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/tcprelay/mod.rs @@ -64,9 +64,10 @@ async fn handle_redir_client( // // Try to convert IPv4 mapped IPv6 address for dual-stack mode. if let SocketAddr::V6(ref a) = daddr - && let Some(v4) = to_ipv4_mapped(a.ip()) { - daddr = SocketAddr::new(IpAddr::from(v4), a.port()); - } + && let Some(v4) = to_ipv4_mapped(a.ip()) + { + daddr = SocketAddr::new(IpAddr::from(v4), a.port()); + } let target_addr = Address::from(daddr); establish_client_tcp_redir(context, balancer, s, peer_addr, &target_addr).await } diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/linux.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/linux.rs index 84579d0d97..c9e821aa30 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/linux.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/linux.rs @@ -71,16 +71,15 @@ impl UdpRedirSocket { socket.set_nonblocking(true)?; socket.set_reuse_address(true)?; - if reuse_port - && let Err(err) = socket.set_reuse_port(true) { - if let Some(libc::ENOPROTOOPT) = err.raw_os_error() { - // SO_REUSEPORT is supported after 3.9 - trace!("failed to set SO_REUSEPORT, error: {}", err); - } else { - error!("failed to set SO_REUSEPORT, error: {}", err); - return Err(err); - } + if reuse_port && let Err(err) = socket.set_reuse_port(true) { + if let Some(libc::ENOPROTOOPT) = err.raw_os_error() { + // SO_REUSEPORT is supported after 3.9 + trace!("failed to set SO_REUSEPORT, error: {}", err); + } else { + error!("failed to set SO_REUSEPORT, error: {}", err); + return Err(err); } + } let sock_addr = SockAddr::from(addr); diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/macos.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/macos.rs index cc6bc65625..b5235624ca 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/macos.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/redir/udprelay/sys/unix/macos.rs @@ -52,15 +52,14 @@ impl UdpRedirSocket { socket.set_nonblocking(true)?; socket.set_reuse_address(true)?; - if reuse_port - && let Err(err) = socket.set_reuse_port(true) { - if let Some(libc::ENOPROTOOPT) = err.raw_os_error() { - trace!("failed to set SO_REUSEPORT, error: {}", err); - } else { - error!("failed to set SO_REUSEPORT, error: {}", err); - return Err(err); - } + if reuse_port && let Err(err) = socket.set_reuse_port(true) { + if let Some(libc::ENOPROTOOPT) = err.raw_os_error() { + trace!("failed to set SO_REUSEPORT, error: {}", err); + } else { + error!("failed to set SO_REUSEPORT, error: {}", err); + return Err(err); } + } let sock_addr = SockAddr::from(addr); diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/mod.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/mod.rs index deecf51430..843ae0fc77 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/mod.rs @@ -11,6 +11,8 @@ pub use self::server::{SocksTcpServer, SocksTcpServerBuilder, SocksUdpServer}; use self::socks5::Socks5UdpServerBuilder; use super::config::Socks5AuthConfig; +#[cfg(feature = "local-http")] +use crate::local::http::config::HttpAuthConfig; #[allow(clippy::module_inception)] mod server; @@ -33,6 +35,8 @@ pub struct SocksBuilder { launchd_tcp_socket_name: Option, #[cfg(target_os = "macos")] launchd_udp_socket_name: Option, + #[cfg(feature = "local-http")] + http_auth: HttpAuthConfig, } impl SocksBuilder { @@ -58,6 +62,8 @@ impl SocksBuilder { launchd_tcp_socket_name: None, #[cfg(target_os = "macos")] launchd_udp_socket_name: None, + #[cfg(feature = "local-http")] + http_auth: HttpAuthConfig::default(), } } @@ -93,6 +99,12 @@ impl SocksBuilder { self.socks5_auth = p; } + /// Set HTTP Authentication configuration + #[cfg(feature = "local-http")] + pub fn set_http_auth(&mut self, p: HttpAuthConfig) { + self.http_auth = p; + } + /// macOS launchd activate socket #[cfg(target_os = "macos")] pub fn set_launchd_tcp_socket_name(&mut self, n: String) { @@ -144,6 +156,8 @@ impl SocksBuilder { self.balancer.clone(), self.mode, self.socks5_auth, + #[cfg(feature = "local-http")] + self.http_auth, ); #[cfg(target_os = "macos")] diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/server.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/server.rs index 31f2640953..f6851dca5b 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/server.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/socks/server/server.rs @@ -5,7 +5,7 @@ use shadowsocks::{ServerAddr, config::Mode, net::TcpListener as ShadowTcpListene use tokio::{net::TcpStream, time}; #[cfg(feature = "local-http")] -use crate::local::http::HttpConnectionHandler; +use crate::local::http::{HttpConnectionHandler, config::HttpAuthConfig}; use crate::local::{ context::ServiceContext, loadbalancing::PingBalancer, net::tcp::listener::create_standard_tcp_listener, socks::config::Socks5AuthConfig, @@ -24,6 +24,8 @@ pub struct SocksTcpServerBuilder { socks5_auth: Arc, #[cfg(target_os = "macos")] launchd_socket_name: Option, + #[cfg(feature = "local-http")] + http_auth: Arc, } impl SocksTcpServerBuilder { @@ -34,6 +36,7 @@ impl SocksTcpServerBuilder { balancer: PingBalancer, mode: Mode, socks5_auth: Socks5AuthConfig, + #[cfg(feature = "local-http")] http_auth: HttpAuthConfig, ) -> Self { Self { context, @@ -44,6 +47,8 @@ impl SocksTcpServerBuilder { socks5_auth: Arc::new(socks5_auth), #[cfg(target_os = "macos")] launchd_socket_name: None, + #[cfg(feature = "local-http")] + http_auth: Arc::new(http_auth), } } @@ -80,6 +85,8 @@ impl SocksTcpServerBuilder { balancer: self.balancer, mode: self.mode, socks5_auth: self.socks5_auth, + #[cfg(feature = "local-http")] + http_auth: self.http_auth, }) } } @@ -92,6 +99,8 @@ pub struct SocksTcpServer { balancer: PingBalancer, mode: Mode, socks5_auth: Arc, + #[cfg(feature = "local-http")] + http_auth: Arc, } impl SocksTcpServer { @@ -107,7 +116,8 @@ impl SocksTcpServer { // If UDP is enabled, SOCK5 UDP_ASSOCIATE command will let client to send requests to this address let udp_associate_addr = Arc::new(self.udp_associate_addr); #[cfg(feature = "local-http")] - let http_handler = HttpConnectionHandler::new(self.context.clone(), self.balancer.clone()); + let http_handler = + HttpConnectionHandler::new(self.context.clone(), self.balancer.clone(), self.http_auth.clone()); loop { let (stream, peer_addr) = match self.listener.accept().await { diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/tun/tcp.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/tun/tcp.rs index dcbcc49ea2..df3f24a91e 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/tun/tcp.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/tun/tcp.rs @@ -160,9 +160,10 @@ impl AsyncRead for TcpConnection { // Nothing could be read. Wait for notify. if let Some(old_waker) = control.recv_waker.replace(cx.waker().clone()) - && !old_waker.will_wake(cx.waker()) { - old_waker.wake(); - } + && !old_waker.will_wake(cx.waker()) + { + old_waker.wake(); + } return Poll::Pending; } @@ -191,9 +192,10 @@ impl AsyncWrite for TcpConnection { if control.send_buffer.is_full() { if let Some(old_waker) = control.send_waker.replace(cx.waker().clone()) - && !old_waker.will_wake(cx.waker()) { - old_waker.wake(); - } + && !old_waker.will_wake(cx.waker()) + { + old_waker.wake(); + } return Poll::Pending; } @@ -223,9 +225,10 @@ impl AsyncWrite for TcpConnection { } if let Some(old_waker) = control.send_waker.replace(cx.waker().clone()) - && !old_waker.will_wake(cx.waker()) { - old_waker.wake(); - } + && !old_waker.will_wake(cx.waker()) + { + old_waker.wake(); + } self.manager_notify.notify(); Poll::Pending @@ -418,10 +421,12 @@ impl TcpTun { wake_receiver = true; } - if wake_receiver && control.recv_waker.is_some() - && let Some(waker) = control.recv_waker.take() { - waker.wake(); - } + if wake_receiver + && control.recv_waker.is_some() + && let Some(waker) = control.recv_waker.take() + { + waker.wake(); + } // Check if writable let mut wake_sender = false; @@ -452,10 +457,12 @@ impl TcpTun { } } - if wake_sender && control.send_waker.is_some() - && let Some(waker) = control.send_waker.take() { - waker.wake(); - } + if wake_sender + && control.send_waker.is_some() + && let Some(waker) = control.send_waker.take() + { + waker.wake(); + } } for socket_handle in sockets_to_remove { @@ -597,9 +604,10 @@ async fn handle_redir_client( // // Try to convert IPv4 mapped IPv6 address for dual-stack mode. if let SocketAddr::V6(ref a) = daddr - && let Some(v4) = to_ipv4_mapped(a.ip()) { - daddr = SocketAddr::new(IpAddr::from(v4), a.port()); - } + && let Some(v4) = to_ipv4_mapped(a.ip()) + { + daddr = SocketAddr::new(IpAddr::from(v4), a.port()); + } let target_addr = Address::from(daddr); establish_client_tcp_redir(context, balancer, s, peer_addr, &target_addr).await } diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/manager/server.rs b/shadowsocks-rust/crates/shadowsocks-service/src/manager/server.rs index c8d939220c..2b5e330548 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/manager/server.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/manager/server.rs @@ -330,22 +330,23 @@ impl Manager { let pid_path = self.server_pid_path(port); if pid_path.exists() - && let Ok(mut pid_file) = File::open(&pid_path) { - let mut pid_content = String::new(); - if pid_file.read_to_string(&mut pid_content).is_ok() { - let pid_content = pid_content.trim(); + && let Ok(mut pid_file) = File::open(&pid_path) + { + let mut pid_content = String::new(); + if pid_file.read_to_string(&mut pid_content).is_ok() { + let pid_content = pid_content.trim(); - match pid_content.parse::() { - Ok(pid) => { - let _ = unsafe { libc::kill(pid, libc::SIGTERM) }; - debug!("killed standalone server port {}, pid: {}", port, pid); - } - Err(..) => { - warn!("failed to read pid from {}", pid_path.display()); - } + match pid_content.parse::() { + Ok(pid) => { + let _ = unsafe { libc::kill(pid, libc::SIGTERM) }; + debug!("killed standalone server port {}, pid: {}", port, pid); + } + Err(..) => { + warn!("failed to read pid from {}", pid_path.display()); } } } + } let server_config_path = self.server_config_path(port); diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/server/udprelay.rs b/shadowsocks-rust/crates/shadowsocks-service/src/server/udprelay.rs index 4ee0bee9d2..0816388d77 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/server/udprelay.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/server/udprelay.rs @@ -594,13 +594,14 @@ impl UdpAssociationContext { control: &Option, ) { if let Some(ref mut session) = self.client_session - && peer_addr != self.peer_addr { - debug!( - "udp relay for {} changed to {}, session: {:?}", - self.peer_addr, peer_addr, session.client_session_id - ); - self.peer_addr = peer_addr; - } + && peer_addr != self.peer_addr + { + debug!( + "udp relay for {} changed to {}, session: {:?}", + self.peer_addr, peer_addr, session.client_session_id + ); + self.peer_addr = peer_addr; + } trace!( "udp relay {} -> {} with {} bytes, control: {:?}", @@ -737,9 +738,10 @@ impl UdpAssociationContext { // But for some implementations, they may expect the target address to be IPv4, because // the peer address is IPv4 when calling `sendto`. if let Address::SocketAddress(SocketAddr::V6(ref v6)) = addr - && let Some(v4) = to_ipv4_mapped(v6.ip()) { - addr = Address::SocketAddress(SocketAddr::new(v4.into(), v6.port())); - } + && let Some(v4) = to_ipv4_mapped(v6.ip()) + { + addr = Address::SocketAddress(SocketAddr::new(v4.into(), v6.port())); + } match self.client_session { None => { diff --git a/shadowsocks-rust/crates/shadowsocks/src/config.rs b/shadowsocks-rust/crates/shadowsocks/src/config.rs index 4491c75570..bedda61508 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/config.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/config.rs @@ -696,18 +696,20 @@ impl ServerConfig { /// Get server's TCP external address pub fn tcp_external_addr(&self) -> &ServerAddr { if let Some(plugin) = self.plugin() - && plugin.plugin_mode.enable_tcp() { - return self.plugin_addr.as_ref().unwrap_or(&self.addr); - } + && plugin.plugin_mode.enable_tcp() + { + return self.plugin_addr.as_ref().unwrap_or(&self.addr); + } &self.addr } /// Get server's UDP external address pub fn udp_external_addr(&self) -> &ServerAddr { if let Some(plugin) = self.plugin() - && plugin.plugin_mode.enable_udp() { - return self.plugin_addr.as_ref().unwrap_or(&self.addr); - } + && plugin.plugin_mode.enable_udp() + { + return self.plugin_addr.as_ref().unwrap_or(&self.addr); + } &self.addr } diff --git a/shadowsocks-rust/crates/shadowsocks/src/net/sys/mod.rs b/shadowsocks-rust/crates/shadowsocks/src/net/sys/mod.rs index 7e2b1ade03..ff62d3da79 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/net/sys/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/net/sys/mod.rs @@ -168,13 +168,14 @@ static IP_STACK_CAPABILITIES: LazyLock = LazyLock::new(|| { // Check IPv6 (::1) if let Ok(ipv6_socket) = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP)) - && ipv6_socket.set_only_v6(true).is_ok() { - let local_host = SockAddr::from(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0)); - if ipv6_socket.bind(&local_host).is_ok() { - caps.support_ipv6 = true; - debug!("IpStackCapability support_ipv6=true"); - } + && ipv6_socket.set_only_v6(true).is_ok() + { + let local_host = SockAddr::from(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0)); + if ipv6_socket.bind(&local_host).is_ok() { + caps.support_ipv6 = true; + debug!("IpStackCapability support_ipv6=true"); } + } // Check IPv4-mapped-IPv6 (127.0.0.1) if check_ipv4_mapped_ipv6_capability().is_ok() { diff --git a/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/bsd/macos.rs b/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/bsd/macos.rs index b760f9a751..fc97a29858 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/bsd/macos.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/bsd/macos.rs @@ -391,9 +391,10 @@ pub async fn bind_outbound_udp_socket(bind_addr: &SocketAddr, config: &ConnectOp }; if !config.udp.allow_fragmentation - && let Err(err) = set_disable_ip_fragmentation(af, &socket) { - warn!("failed to disable IP fragmentation, error: {}", err); - } + && let Err(err) = set_disable_ip_fragmentation(af, &socket) + { + warn!("failed to disable IP fragmentation, error: {}", err); + } // Set IP_BOUND_IF for BSD-like if let Some(ref iface) = config.bind_interface { diff --git a/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/linux/mod.rs b/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/linux/mod.rs index b77c7bf5c9..4f13a5841c 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/linux/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/net/sys/unix/linux/mod.rs @@ -311,9 +311,10 @@ pub async fn bind_outbound_udp_socket(bind_addr: &SocketAddr, config: &ConnectOp }; if !config.udp.allow_fragmentation - && let Err(err) = set_disable_ip_fragmentation(af, &socket) { - warn!("failed to disable IP fragmentation, error: {}", err); - } + && let Err(err) = set_disable_ip_fragmentation(af, &socket) + { + warn!("failed to disable IP fragmentation, error: {}", err); + } // Any traffic except localhost should be protected // This is a workaround for VPNService diff --git a/shadowsocks-rust/crates/shadowsocks/src/net/udp.rs b/shadowsocks-rust/crates/shadowsocks/src/net/udp.rs index 00ac8c1d36..2bd6509e56 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/net/udp.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/net/udp.rs @@ -35,11 +35,11 @@ use futures::ready; use tokio::io::Interest; use tokio::{io::ReadBuf, net::ToSocketAddrs}; -use crate::{context::Context, relay::socks5::Address, ServerAddr}; +use crate::{ServerAddr, context::Context, relay::socks5::Address}; use super::{ - sys::{bind_outbound_udp_socket, create_inbound_udp_socket, create_outbound_udp_socket}, AcceptOpts, AddrFamily, ConnectOpts, + sys::{bind_outbound_udp_socket, create_inbound_udp_socket, create_outbound_udp_socket}, }; /// Message struct for `batch_send` @@ -190,9 +190,10 @@ impl UdpSocket { pub fn poll_send(&self, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll> { // Check MTU if let Some(mtu) = self.mtu - && buf.len() > mtu { - return Err(make_mtu_error(buf.len(), mtu)).into(); - } + && buf.len() > mtu + { + return Err(make_mtu_error(buf.len(), mtu)).into(); + } self.socket.poll_send(cx, buf) } @@ -202,9 +203,10 @@ impl UdpSocket { pub async fn send(&self, buf: &[u8]) -> io::Result { // Check MTU if let Some(mtu) = self.mtu - && buf.len() > mtu { - return Err(make_mtu_error(buf.len(), mtu)); - } + && buf.len() > mtu + { + return Err(make_mtu_error(buf.len(), mtu)); + } self.socket.send(buf).await } @@ -213,9 +215,10 @@ impl UdpSocket { pub fn poll_send_to(&self, cx: &mut TaskContext<'_>, buf: &[u8], target: SocketAddr) -> Poll> { // Check MTU if let Some(mtu) = self.mtu - && buf.len() > mtu { - return Err(make_mtu_error(buf.len(), mtu)).into(); - } + && buf.len() > mtu + { + return Err(make_mtu_error(buf.len(), mtu)).into(); + } self.socket.poll_send_to(cx, buf, target) } @@ -225,9 +228,10 @@ impl UdpSocket { pub async fn send_to(&self, buf: &[u8], target: A) -> io::Result { // Check MTU if let Some(mtu) = self.mtu - && buf.len() > mtu { - return Err(make_mtu_error(buf.len(), mtu)); - } + && buf.len() > mtu + { + return Err(make_mtu_error(buf.len(), mtu)); + } self.socket.send_to(buf, target).await } @@ -238,9 +242,10 @@ impl UdpSocket { ready!(self.socket.poll_recv(cx, buf))?; if let Some(mtu) = self.mtu - && buf.filled().len() > mtu { - return Err(make_mtu_error(buf.filled().len(), mtu)).into(); - } + && buf.filled().len() > mtu + { + return Err(make_mtu_error(buf.filled().len(), mtu)).into(); + } Ok(()).into() } @@ -251,9 +256,10 @@ impl UdpSocket { let n = self.socket.recv(buf).await?; if let Some(mtu) = self.mtu - && n > mtu { - return Err(make_mtu_error(n, mtu)); - } + && n > mtu + { + return Err(make_mtu_error(n, mtu)); + } Ok(n) } @@ -264,9 +270,10 @@ impl UdpSocket { let addr = ready!(self.socket.poll_recv_from(cx, buf))?; if let Some(mtu) = self.mtu - && buf.filled().len() > mtu { - return Err(make_mtu_error(buf.filled().len(), mtu)).into(); - } + && buf.filled().len() > mtu + { + return Err(make_mtu_error(buf.filled().len(), mtu)).into(); + } Ok(addr).into() } @@ -277,9 +284,10 @@ impl UdpSocket { let (n, addr) = self.socket.recv_from(buf).await?; if let Some(mtu) = self.mtu - && n > mtu { - return Err(make_mtu_error(n, mtu)); - } + && n > mtu + { + return Err(make_mtu_error(n, mtu)); + } Ok((n, addr)) } diff --git a/shadowsocks-rust/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs b/shadowsocks-rust/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs index b8d41ee302..96b2aca370 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs @@ -160,9 +160,10 @@ where // Wakeup writer task because we have already received the salt #[cfg(feature = "aead-cipher-2022")] if let ProxyServerStreamWriteState::PrepareHeader(waker) = this.writer_state - && let Some(waker) = waker.take() { - waker.wake(); - } + && let Some(waker) = waker.take() + { + waker.wake(); + } Ok(()).into() } @@ -190,9 +191,10 @@ where } else { // Reader didn't receive the salt from client yet. if let Some(waker) = waker.take() - && !waker.will_wake(cx.waker()) { - waker.wake(); - } + && !waker.will_wake(cx.waker()) + { + waker.wake(); + } *waker = Some(cx.waker().clone()); return Poll::Pending; } diff --git a/shadowsocks-rust/src/daemonize/daemonize/mod.rs b/shadowsocks-rust/src/daemonize/daemonize/mod.rs index 7fa937c106..98b60993b1 100644 --- a/shadowsocks-rust/src/daemonize/daemonize/mod.rs +++ b/shadowsocks-rust/src/daemonize/daemonize/mod.rs @@ -6,7 +6,6 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! //! daemonize is a library for writing system daemons. Inspired by the Python library [thesharp/daemonize](https://github.com/thesharp/daemonize). //! //! The respository is located at . @@ -211,7 +210,6 @@ impl Outcome { /// * change root directory; /// * change the pid-file ownership to provided user (and/or) group; /// * execute any provided action just before dropping privileges. -/// pub struct Daemonize { directory: PathBuf, pid_file: Option, @@ -339,6 +337,7 @@ impl Daemonize { self.stderr = stdio.into(); self } + /// Start daemonization process, terminate parent after first fork, returns privileged action /// result to the child. pub fn start(self) -> Result { diff --git a/shadowsocks-rust/src/service/local.rs b/shadowsocks-rust/src/service/local.rs index 786f2ce586..507d9e9136 100644 --- a/shadowsocks-rust/src/service/local.rs +++ b/shadowsocks-rust/src/service/local.rs @@ -756,14 +756,16 @@ pub fn create(matches: &ArgMatches) -> ShadowsocksResult<(Runtime, impl Future("TCP_REDIR") { - local_config.tcp_redir = tcp_redir.parse::().expect("tcp-redir"); - } + && let Some(tcp_redir) = matches.get_one::("TCP_REDIR") + { + local_config.tcp_redir = tcp_redir.parse::().expect("tcp-redir"); + } if RedirType::udp_default() != RedirType::NotSupported - && let Some(udp_redir) = matches.get_one::("UDP_REDIR") { - local_config.udp_redir = udp_redir.parse::().expect("udp-redir"); - } + && let Some(udp_redir) = matches.get_one::("UDP_REDIR") + { + local_config.udp_redir = udp_redir.parse::().expect("udp-redir"); + } } #[cfg(feature = "local-dns")] diff --git a/shadowsocks-rust/src/service/manager.rs b/shadowsocks-rust/src/service/manager.rs index c0e2d76964..29b19216ab 100644 --- a/shadowsocks-rust/src/service/manager.rs +++ b/shadowsocks-rust/src/service/manager.rs @@ -405,14 +405,16 @@ pub fn create(matches: &ArgMatches) -> ShadowsocksResult<(Runtime, impl Future("ACL") { let acl = AccessControl::load_from_file(acl_file) diff --git a/sing-box/.github/CRONET_GO_VERSION b/sing-box/.github/CRONET_GO_VERSION index 74f690d247..73d4820b59 100644 --- a/sing-box/.github/CRONET_GO_VERSION +++ b/sing-box/.github/CRONET_GO_VERSION @@ -1 +1 @@ -446096b34fb07e86541d927be26f8e5c42d41b4a +5e61220bb2d22453eb6d105077c88c8ab1e1c3b2 diff --git a/sing-box/docs/changelog.md b/sing-box/docs/changelog.md index d4db1aace7..77c787ec82 100644 --- a/sing-box/docs/changelog.md +++ b/sing-box/docs/changelog.md @@ -2,6 +2,16 @@ icon: material/alert-decagram --- +#### 1.13.0-alpha.32 + +* Remove `certificate_public_key_sha256` option for NaiveProxy outbound **1** +* Fixes and improvements + +**1**: + +Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis. +For this reason, and due to maintenance costs, there is no reason to continue supporting `certificate_public_key_sha256`, which was designed to simplify the use of self-signed certificates. + #### 1.13.0-alpha.31 * Add QUIC support for NaiveProxy outbound **1** diff --git a/sing-box/docs/configuration/inbound/naive.md b/sing-box/docs/configuration/inbound/naive.md index 05c5b9f992..a360fa95b0 100644 --- a/sing-box/docs/configuration/inbound/naive.md +++ b/sing-box/docs/configuration/inbound/naive.md @@ -1,3 +1,7 @@ +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [quic_congestion_control](#quic_congestion_control) + ### Structure ```json @@ -39,6 +43,8 @@ Naive users. #### quic_congestion_control +!!! question "Since sing-box 1.13.0" + QUIC congestion control algorithm. | Algorithm | Description | diff --git a/sing-box/docs/configuration/inbound/naive.zh.md b/sing-box/docs/configuration/inbound/naive.zh.md index bc2620b791..c9bfc9173d 100644 --- a/sing-box/docs/configuration/inbound/naive.zh.md +++ b/sing-box/docs/configuration/inbound/naive.zh.md @@ -1,3 +1,7 @@ +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [quic_congestion_control](#quic_congestion_control) + ### 结构 ```json @@ -39,6 +43,8 @@ Naive 用户。 #### quic_congestion_control +!!! question "Since sing-box 1.13.0" + QUIC 拥塞控制算法。 | 算法 | 描述 | diff --git a/sing-box/docs/configuration/outbound/naive.md b/sing-box/docs/configuration/outbound/naive.md index a56036d960..d9af4fb1d6 100644 --- a/sing-box/docs/configuration/outbound/naive.md +++ b/sing-box/docs/configuration/outbound/naive.md @@ -105,7 +105,9 @@ QUIC congestion control algorithm. TLS configuration, see [TLS](/configuration/shared/tls/#outbound). -Only `server_name`, `certificate`, `certificate_path`, `certificate_public_key_sha256` and `ech` are supported. +Only `server_name`, `certificate`, `certificate_path` and `ech` are supported. + +Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis, and should not be used in production. ### Dial Fields diff --git a/sing-box/docs/configuration/outbound/naive.zh.md b/sing-box/docs/configuration/outbound/naive.zh.md index 40bcc2e562..0789640799 100644 --- a/sing-box/docs/configuration/outbound/naive.zh.md +++ b/sing-box/docs/configuration/outbound/naive.zh.md @@ -105,7 +105,9 @@ QUIC 拥塞控制算法。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 -只有 `server_name`、`certificate`、`certificate_path`、`certificate_public_key_sha256` 和 `ech` 是被支持的。 +只有 `server_name`、`certificate`、`certificate_path` 和 `ech` 是被支持的。 + +自签名证书会显著改变流量行为,违背了 NaiveProxy 旨在抵抗流量分析的设计初衷,不应该在生产环境中使用。 ### 拨号字段 diff --git a/sing-box/docs/configuration/shared/tls.md b/sing-box/docs/configuration/shared/tls.md index a67ada08d0..c388061dc2 100644 --- a/sing-box/docs/configuration/shared/tls.md +++ b/sing-box/docs/configuration/shared/tls.md @@ -4,16 +4,16 @@ icon: material/new-box !!! quote "Changes in sing-box 1.13.0" - :material-plus: [kernel_tx](#kernel_tx) - :material-plus: [kernel_rx](#kernel_rx) - :material-plus: [curve_preferences](#curve_preferences) - :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) - :material-plus: [client_certificate](#client_certificate) - :material-plus: [client_certificate_path](#client_certificate_path) - :material-plus: [client_key](#client_key) - :material-plus: [client_key_path](#client_key_path) - :material-plus: [client_authentication](#client_authentication) - :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) + :material-plus: [kernel_tx](#kernel_tx) + :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [curve_preferences](#curve_preferences) + :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) + :material-plus: [client_certificate](#client_certificate) + :material-plus: [client_certificate_path](#client_certificate_path) + :material-plus: [client_key](#client_key) + :material-plus: [client_key_path](#client_key_path) + :material-plus: [client_authentication](#client_authentication) + :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) :material-plus: [ech.query_server_name](#query_server_name) !!! quote "Changes in sing-box 1.12.0" diff --git a/sing-box/docs/configuration/shared/tls.zh.md b/sing-box/docs/configuration/shared/tls.zh.md index e51e45112d..db799feaea 100644 --- a/sing-box/docs/configuration/shared/tls.zh.md +++ b/sing-box/docs/configuration/shared/tls.zh.md @@ -4,16 +4,16 @@ icon: material/new-box !!! quote "sing-box 1.13.0 中的更改" - :material-plus: [kernel_tx](#kernel_tx) - :material-plus: [kernel_rx](#kernel_rx) - :material-plus: [curve_preferences](#curve_preferences) - :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) - :material-plus: [client_certificate](#client_certificate) - :material-plus: [client_certificate_path](#client_certificate_path) - :material-plus: [client_key](#client_key) - :material-plus: [client_key_path](#client_key_path) - :material-plus: [client_authentication](#client_authentication) - :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) + :material-plus: [kernel_tx](#kernel_tx) + :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [curve_preferences](#curve_preferences) + :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) + :material-plus: [client_certificate](#client_certificate) + :material-plus: [client_certificate_path](#client_certificate_path) + :material-plus: [client_key](#client_key) + :material-plus: [client_key_path](#client_key_path) + :material-plus: [client_authentication](#client_authentication) + :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) :material-plus: [ech.query_server_name](#query_server_name) !!! quote "sing-box 1.12.0 中的更改" diff --git a/sing-box/go.mod b/sing-box/go.mod index 85a9f52a01..1c2fe80ad9 100644 --- a/sing-box/go.mod +++ b/sing-box/go.mod @@ -26,8 +26,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20251218041957-eeb28f1dba2f - github.com/sagernet/cronet-go/all v0.0.0-20251218041957-eeb28f1dba2f + github.com/sagernet/cronet-go v0.0.0-20251219080614-460b6a5fb79d + github.com/sagernet/cronet-go/all v0.0.0-20251219080614-460b6a5fb79d github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.10 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -108,28 +108,28 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20251218041530-c2a431c5f1f7 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20251219080137-fbc9bf07b76b // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20251219080137-fbc9bf07b76b // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.6 // indirect diff --git a/sing-box/go.sum b/sing-box/go.sum index 02c0e15ae8..725da22ec8 100644 --- a/sing-box/go.sum +++ b/sing-box/go.sum @@ -154,54 +154,54 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20251218041957-eeb28f1dba2f h1:WTHyVtd9nNZ4VB20aja31e0ZXXGrVlssAanJJBMc5BU= -github.com/sagernet/cronet-go v0.0.0-20251218041957-eeb28f1dba2f/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20251218041957-eeb28f1dba2f h1:qupezSQEMraq2yajI4HrWf9h9rY7RESUYbYHRRd49FY= -github.com/sagernet/cronet-go/all v0.0.0-20251218041957-eeb28f1dba2f/go.mod h1:onaFo5hJh1KsvuxkYTFLokh0Yx2oBh9J3yvFTnFB1Fc= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20251218041530-c2a431c5f1f7 h1:jmDdDxVFN/W+pX/QRHYR6jFu5gosRPNSSPemGmchHpM= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20251218041530-c2a431c5f1f7 h1:zQeDQJg2YZSlmR5+mYV4KyqIWwe+SH5hnxTO4EalDwA= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20251218041530-c2a431c5f1f7 h1:85ck9+7Ftj+J3RO1uusn1Y3EXRLX9Dy0WIK/ZjRlQok= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20251218041530-c2a431c5f1f7 h1:D//nc8RxHp7tTKcIYYQZ80wQBwxlCJv6HpkrCkqIVRo= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20251218041530-c2a431c5f1f7 h1:wwrQCcrwnHSY/Y4cy8JCiZLzuWXM8tENphhJtFplh0c= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20251218041530-c2a431c5f1f7 h1:Q52ojQVXk9f6Z+EWHWv+SJajNF56bVvgMyQP/7WV2TI= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20251218041530-c2a431c5f1f7 h1:UzXQ09sZQZDlsZvWd4SAhtaF0RMxAVg4TEYsRNMutjU= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20251218041530-c2a431c5f1f7 h1:sjSknfRi3fMJqwHW4pNL64SGq7Y5XIm4pz5Ds21hvKw= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20251218041530-c2a431c5f1f7 h1:uxb8pqzB4KH7ai6MocOWQPu3/+BAYISSIMPHWs0wtrQ= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20251218041530-c2a431c5f1f7 h1:uV78fIQ2XdVQUzowiafKP2fs+rGlkI+9u7l1cBMlQlc= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20251218041530-c2a431c5f1f7 h1:6d87Pvio5dpWakic3SNBVTwTlt8ZnvL1GWPPeY2xSLs= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20251218041530-c2a431c5f1f7 h1:tc3GdN9pAfZqdfb55DmtUSXEuKuecWNYSD/+4jclXmo= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20251218041530-c2a431c5f1f7 h1:W5fN1B1wYgnh7Y5Nw+MgcDjsvdPvEPAGe27iemq2LJs= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20251218041530-c2a431c5f1f7 h1:xfNH8CO8p2W8rtVFgyv8kiAkzoSArW6+h2s/p5ZNvP8= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20251218041530-c2a431c5f1f7 h1:1mkhgxLOh1pPjBLgC/GK3Rhhinz0254OjGJRrivVhQ8= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20251218041530-c2a431c5f1f7 h1:zf252WwVhJYduTDBXvkBKdwSU+Ce0OToQzvy5SwPS3c= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20251218041530-c2a431c5f1f7 h1:Nhkn2M2UsGe0hr6qOgNKvGKgAbo6EfWQto+YjlNWGqg= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20251218041530-c2a431c5f1f7 h1:HSvWFyBDLC7grFuSwBSIaxLI/MVpe/w4qJJAWAZsBxc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20251218041530-c2a431c5f1f7 h1:PJa1fhmbXWXCYk07DZhqzjP2u9rM/cnS0A1aaPU56pY= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20251218041530-c2a431c5f1f7 h1:Tpskq8p8boYHkBya18ythhgWTMv8gFsseGk55MFW+/k= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20251218041530-c2a431c5f1f7 h1:qjB64lzTP0ROuk9GCCwsNQ9ca37nIr75iOyc2vuGFQg= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20251218041530-c2a431c5f1f7 h1:GDJjygR/HLWOzwAs0RjhvqN6d1rUm7WiDAmHNe8gRAM= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20251218041530-c2a431c5f1f7/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20251219080614-460b6a5fb79d h1:Wfs4UEnHE3d0hHHG1NedCCwU2lUVlsgKblBGuOps8k4= +github.com/sagernet/cronet-go v0.0.0-20251219080614-460b6a5fb79d/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20251219080614-460b6a5fb79d h1:DZy1I3zKP5bRaFGDYStZWAXrtlxn3W4ONbFUD027sw8= +github.com/sagernet/cronet-go/all v0.0.0-20251219080614-460b6a5fb79d/go.mod h1:eKeQLIddaQGZD2/jc0t+AdTnHve3Yo+uMujwgNOCfz0= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20251219080137-fbc9bf07b76b h1:+WRqXRiHEom7+lZUEA4qPVfNQB7Rx4++6JEN8cd5CgQ= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20251219080137-fbc9bf07b76b h1:ofD/ZwijuG5ccz0afLcvbfssIG+q/Bp8A2sziYYlJ28= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20251219080137-fbc9bf07b76b h1:m3Hv8Hs6x6JdFNe93nzh1XhdZ7DmkfOKheloDmQmGQo= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20251219080137-fbc9bf07b76b h1:gA8bzjowa5Lmih+hBjjh6xsjghmGTIojKNtPzVDG93A= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20251219080137-fbc9bf07b76b h1:0ASeUCDj66dAHaCU/HkgbuSm/1co+yHbrq13BokW/us= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20251219080137-fbc9bf07b76b h1:NsbDT3L2pLS0Qsh0MPEn+IPkL33XoL7eFs56EA0jBeo= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20251219080137-fbc9bf07b76b h1:hIMIG2AuF3n9yelPvfUB3jyW2vWJnPiJfhE3hGUGJDk= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20251219080137-fbc9bf07b76b h1:/l1dS38ijnn0IHtAdFH3Y8cahAGw6ii0TYow0jUNBGM= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20251219080137-fbc9bf07b76b h1:m29qbSNfdNuTYEgTkph9TQUIyu7wIrY3PAvbgl23OjA= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20251219080137-fbc9bf07b76b h1:mmOPED30mKTr0DucTYQeJBHtjAgvBO7huwdG3gL6Gyw= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20251219080137-fbc9bf07b76b h1:y7V2c5QwMY07ME/M+cI/W1ZNw2QFT2oMw99GaPtK0MY= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20251219080137-fbc9bf07b76b h1:txNHLfT1miemCZgCrtrA+eTbGfsfN46wHIpOShALNzI= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20251219080137-fbc9bf07b76b h1:FVPZ3TKAoIeM1hTCqUn0D7gHx6rDkKqhx+E8yaYNbXI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20251219080137-fbc9bf07b76b h1:4ei7Sz97XCFXh+FUi2cKGHrhqD0A8lxdjZxST7NzDrQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20251219080137-fbc9bf07b76b h1:W0SrTDGPaKfcgjiZDkYeWqpICeVkwydaa228znP4q2c= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20251219080137-fbc9bf07b76b h1:WD7V83I/FDatR0zXq6gSxh8gGZCD+fUZjLb1f+OVuF0= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20251219080137-fbc9bf07b76b h1:M1V7j6Bke+v9aXeEK/NcWqoPwl84h97fmL3XSJJmNJw= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20251219080137-fbc9bf07b76b h1:DmLB0sgGw7vNao08a7NJ0xMJbZpeL49rAphknfRRs5I= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20251219080137-fbc9bf07b76b h1:j8tKUY2VVnK/RxIvX75G/nLGu53to6LaDRH/NTzyLDw= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20251219080137-fbc9bf07b76b h1:+1hjEBE2hUGUxDKtNg+gBYYCHXPQao273PjimC67v38= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20251219080137-fbc9bf07b76b h1:22Jx/y6cz2lVeUwbN/lmNOUjhEWYpohGEYpW/zdDcBk= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20251219080137-fbc9bf07b76b h1:7juUKsCbreYSP8gALPf45kut0zdSpZXrk2TLIwQ2zOo= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20251219080137-fbc9bf07b76b/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.10 h1:ElqZ0OVDvyQlU91MU0C9cfU0FrILBbc65+NOKzZ1t0c= diff --git a/sing-box/protocol/naive/outbound.go b/sing-box/protocol/naive/outbound.go index 8bda26ec15..53e3e3a481 100644 --- a/sing-box/protocol/naive/outbound.go +++ b/sing-box/protocol/naive/outbound.go @@ -176,22 +176,21 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, E.New("unknown quic congestion control: ", options.QUICCongestionControl) } client, err := cronet.NewNaiveClient(cronet.NaiveClientConfig{ - Context: ctx, - ServerAddress: serverAddress, - ServerName: serverName, - Username: options.Username, - Password: options.Password, - InsecureConcurrency: options.InsecureConcurrency, - ExtraHeaders: extraHeaders, - TrustedRootCertificates: trustedRootCertificates, - TrustedCertificatePublicKeySHA256: options.TLS.CertificatePublicKeySHA256, - Dialer: outboundDialer, - DNSResolver: dnsResolver, - ECHEnabled: echEnabled, - ECHConfigList: echConfigList, - ECHQueryServerName: echQueryServerName, - QUIC: options.QUIC, - QUICCongestionControl: quicCongestionControl, + Context: ctx, + ServerAddress: serverAddress, + ServerName: serverName, + Username: options.Username, + Password: options.Password, + InsecureConcurrency: options.InsecureConcurrency, + ExtraHeaders: extraHeaders, + TrustedRootCertificates: trustedRootCertificates, + Dialer: outboundDialer, + DNSResolver: dnsResolver, + ECHEnabled: echEnabled, + ECHConfigList: echConfigList, + ECHQueryServerName: echQueryServerName, + QUIC: options.QUIC, + QUICCongestionControl: quicCongestionControl, }) if err != nil { return nil, err diff --git a/sing-box/test/naive_self_test.go b/sing-box/test/naive_self_test.go index a40f2f78b9..873e7b9ef1 100644 --- a/sing-box/test/naive_self_test.go +++ b/sing-box/test/naive_self_test.go @@ -1,9 +1,6 @@ package main import ( - "crypto/sha256" - "crypto/x509" - "encoding/pem" "net/netip" "os" "strings" @@ -109,106 +106,6 @@ func TestNaiveSelf(t *testing.T) { testTCP(t, clientPort, testPort) } -func TestNaiveSelfPublicKeySHA256(t *testing.T) { - _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") - - // Read and parse the server certificate to get its public key SHA256 - certPemContent, err := os.ReadFile(certPem) - require.NoError(t, err) - block, _ := pem.Decode(certPemContent) - require.NotNil(t, block) - cert, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - - // Calculate SHA256 of SPKI (Subject Public Key Info) - spkiBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) - require.NoError(t, err) - pinHash := sha256.Sum256(spkiBytes) - - startInstance(t, option.Options{ - Inbounds: []option.Inbound{ - { - Type: C.TypeMixed, - Tag: "mixed-in", - Options: &option.HTTPMixedInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), - ListenPort: clientPort, - }, - }, - }, - { - Type: C.TypeNaive, - Tag: "naive-in", - Options: &option.NaiveInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), - ListenPort: serverPort, - }, - Users: []auth.User{ - { - Username: "sekai", - Password: "password", - }, - }, - Network: network.NetworkTCP, - InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, - }, - }, - }, - }, - }, - Outbounds: []option.Outbound{ - { - Type: C.TypeDirect, - }, - { - Type: C.TypeNaive, - Tag: "naive-out", - Options: &option.NaiveOutboundOptions{ - ServerOptions: option.ServerOptions{ - Server: "127.0.0.1", - ServerPort: serverPort, - }, - Username: "sekai", - Password: "password", - OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePublicKeySHA256: [][]byte{pinHash[:]}, - }, - }, - }, - }, - }, - Route: &option.RouteOptions{ - Rules: []option.Rule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - RawDefaultRule: option.RawDefaultRule{ - Inbound: []string{"mixed-in"}, - }, - RuleAction: option.RuleAction{ - Action: C.RuleActionTypeRoute, - RouteOptions: option.RouteActionOptions{ - Outbound: "naive-out", - }, - }, - }, - }, - }, - }, - }) - testTCP(t, clientPort, testPort) -} - func TestNaiveSelfECH(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") caPemContent, err := os.ReadFile(caPem) diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js index ed47f2f3d9..a97a381b67 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -870,6 +870,28 @@ function generateRand(type, length) { }; } +function shuffle(StrORArr) { + let arr; + + if (typeof StrORArr === 'string') + arr = StrORArr.split(''); + else if (Array.isArray(StrORArr)) + arr = StrORArr; + else + throw new Error(`String or Array only`); + + for (let i = arr.length - 1; i > 0; i--) { // Traverse the array from back to front + const j = Math.floor(Math.random() * (i + 1)); // Generate a random index between 0 and i + + [arr[i], arr[j]] = [arr[j], arr[i]]; // Swap positions + } + + if (typeof StrORArr === 'string') + return arr.join(''); + else if (Array.isArray(StrORArr)) + return arr; +} + function json2yaml(object, command) { const callJson2Yaml = rpc.declare({ object: 'luci.fchomo', @@ -1166,6 +1188,8 @@ function handleGenKey(option) { if (option === 'uuid' || option.match(/_uuid/)) required_method = 'uuid'; + else if (option.match(/sudoku_custom_table/)) + required_method = 'sudoku_custom_table'; else if (type === 'shadowsocks' && option === 'shadowsocks_password') required_method = this.section.getOption('shadowsocks_chipher')?.formvalue(section_id); else if (type === 'trojan' && option === 'trojan_ss_password') @@ -1180,6 +1204,10 @@ function handleGenKey(option) { case 'uuid': password = generateRand('uuid'); break; + /* SUDOKU CUSTOM TABLE */ + case 'sudoku_custom_table': + password = shuffle('xxppvvvv'); + break; /* DEFAULT */ default: password = generateRand('hex', 32/2); @@ -1438,6 +1466,22 @@ function validateShadowsocksPassword(encmode, section_id, value) { return true; } +function validateSudokuCustomTable(section_id, value) { + if (!value) + return true; + + if (value.length !== 8) + return _('Expecting: %s').format(_('valid format: 2x, 2p, 4v')); + + const counts = {}; + for (const c of value) + counts[c] = (counts[c] || 0) + 1; + if (!(counts.x === 2 && counts.p === 2 && counts.v === 4)) + return _('Expecting: %s').format(_('valid format: 2x, 2p, 4v')); + + return true; +} + function validateUniqueValue(section_id, value) { if (!value) return _('Expecting: %s').format(_('non-empty value')); @@ -1629,6 +1673,7 @@ return baseclass.extend({ decodeBase64Bin, encodeBase64Bin, generateRand, + shuffle, json2yaml, yaml2json, isEmpty, @@ -1669,6 +1714,7 @@ return baseclass.extend({ validateMTLSClientAuth, validatePresetIDs, validateShadowsocksPassword, + validateSudokuCustomTable, validateUniqueValue, // file operations lsDir, diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js index ca9f9cbc48..ee9bec0f1e 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js @@ -381,6 +381,14 @@ return view.extend({ hm.sudoku_cipher_methods.forEach((res) => { so.value.apply(so, res); }) + so.validate = function(section_id, value) { + const pure_downlink = this.section.getUIElement(section_id, 'sudoku_enable_pure_downlink')?.node.querySelector('input').checked; + + if (value === 'none' && pure_downlink === false) + return _('Expecting: %s').format(_('Chipher must be enabled if obfuscate downlink is disabled.')); + + return true; + } so.depends('type', 'sudoku'); so.modalonly = true; @@ -390,6 +398,11 @@ return view.extend({ so.depends('type', 'sudoku'); so.modalonly = true; + so = ss.taboption('field_general', form.DynamicList, 'sudoku_custom_tables', _('Custom byte layout')); + so.validate = hm.validateSudokuCustomTable; + so.depends('sudoku_table_type', 'prefer_entropy'); + so.modalonly = true; + so = ss.taboption('field_general', form.Value, 'sudoku_padding_min', _('Minimum padding')); so.datatype = 'uinteger'; so.default = 2; @@ -409,6 +422,13 @@ return view.extend({ so.depends('type', 'sudoku'); so.modalonly = true; + so = ss.taboption('field_general', form.Flag, 'sudoku_enable_pure_downlink', _('Enable obfuscate for downlink'), + _('When disabled, downlink ciphertext is split into 6-bit segments, reusing the original padding pool and obfuscate type to reduce downlink overhead.') + '
' + + _('Uplink keeps the Sudoku protocol, and downlink characteristics are consistent with uplink characteristics.')); + so.default = so.enabled; + so.depends('type', 'sudoku'); + so.modalonly = true; + /* Snell fields */ so = ss.taboption('field_general', form.Value, 'snell_psk', _('Pre-shared key')); so.password = true; diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js index 67b9fdb176..ea83afebcd 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js @@ -358,6 +358,14 @@ return view.extend({ hm.sudoku_cipher_methods.forEach((res) => { o.value.apply(o, res); }) + o.validate = function(section_id, value) { + const pure_downlink = this.section.getUIElement(section_id, 'sudoku_enable_pure_downlink')?.node.querySelector('input').checked; + + if (value === 'none' && pure_downlink === false) + return _('Expecting: %s').format(_('Chipher must be enabled if obfuscate downlink is disabled.')); + + return true; + } o.depends('type', 'sudoku'); o.modalonly = true; @@ -367,6 +375,22 @@ return view.extend({ o.depends('type', 'sudoku'); o.modalonly = true; + o = s.taboption('field_general', form.DynamicList, 'sudoku_custom_tables', _('Custom byte layout')); + o.renderWidget = function(/* ... */) { + let node = form.DynamicList.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + class: 'cbi-button cbi-button-positive', + title: _('Generate'), + click: ui.createHandlerFn(this, hm.handleGenKey, this.hm_options || this.option) + }, [ _('Generate') ])); + + return node; + } + o.validate = hm.validateSudokuCustomTable; + o.depends('sudoku_table_type', 'prefer_entropy'); + o.modalonly = true; + o = s.taboption('field_general', form.Value, 'sudoku_padding_min', _('Minimum padding')); o.datatype = 'uinteger'; o.default = 1; @@ -387,6 +411,13 @@ return view.extend({ o.depends('type', 'sudoku'); o.modalonly = true; + o = s.taboption('field_general', form.Flag, 'sudoku_enable_pure_downlink', _('Enable obfuscate for downlink'), + _('When disabled, downlink ciphertext is split into 6-bit segments, reusing the original padding pool and obfuscate type to reduce downlink overhead.') + '
' + + _('Uplink keeps the Sudoku protocol, and downlink characteristics are consistent with uplink characteristics.')); + o.default = o.enabled; + o.depends('type', 'sudoku'); + o.modalonly = true; + /* Tuic fields */ o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID')); o.rmempty = false; diff --git a/small/luci-app-fchomo/po/templates/fchomo.pot b/small/luci-app-fchomo/po/templates/fchomo.pot index 7d8381b2d9..e33d962e69 100644 --- a/small/luci-app-fchomo/po/templates/fchomo.pot +++ b/small/luci-app-fchomo/po/templates/fchomo.pot @@ -5,36 +5,35 @@ msgstr "Content-Type: text/plain; charset=UTF-8" msgid "%s log" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:594 -#: htdocs/luci-static/resources/view/fchomo/client.js:237 -#: htdocs/luci-static/resources/view/fchomo/client.js:267 -#: htdocs/luci-static/resources/view/fchomo/client.js:363 +#: htdocs/luci-static/resources/fchomo.js:558 +#: htdocs/luci-static/resources/fchomo.js:561 +#: htdocs/luci-static/resources/view/fchomo/client.js:293 msgid "(Imported)" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:549 #: htdocs/luci-static/resources/view/fchomo/global.js:555 -#: htdocs/luci-static/resources/view/fchomo/node.js:890 -#: htdocs/luci-static/resources/view/fchomo/node.js:896 -#: htdocs/luci-static/resources/view/fchomo/node.js:904 -#: htdocs/luci-static/resources/view/fchomo/node.js:910 -#: htdocs/luci-static/resources/view/fchomo/server.js:822 -#: htdocs/luci-static/resources/view/fchomo/server.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 +#: htdocs/luci-static/resources/view/fchomo/node.js:911 +#: htdocs/luci-static/resources/view/fchomo/node.js:917 +#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/server.js:816 +#: htdocs/luci-static/resources/view/fchomo/server.js:825 msgid "(mTLS)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:924 -#: htdocs/luci-static/resources/view/fchomo/client.js:925 -#: htdocs/luci-static/resources/view/fchomo/client.js:938 -#: htdocs/luci-static/resources/view/fchomo/client.js:939 -#: htdocs/luci-static/resources/view/fchomo/client.js:1151 -#: htdocs/luci-static/resources/view/fchomo/client.js:1622 -#: htdocs/luci-static/resources/view/fchomo/client.js:1623 -#: htdocs/luci-static/resources/view/fchomo/node.js:1591 -#: htdocs/luci-static/resources/view/fchomo/node.js:1597 -#: htdocs/luci-static/resources/view/fchomo/node.js:1611 -#: htdocs/luci-static/resources/view/fchomo/node.js:1617 +#: htdocs/luci-static/resources/view/fchomo/client.js:930 +#: htdocs/luci-static/resources/view/fchomo/client.js:931 +#: htdocs/luci-static/resources/view/fchomo/client.js:944 +#: htdocs/luci-static/resources/view/fchomo/client.js:945 +#: htdocs/luci-static/resources/view/fchomo/client.js:1153 +#: htdocs/luci-static/resources/view/fchomo/client.js:1612 +#: htdocs/luci-static/resources/view/fchomo/client.js:1613 +#: htdocs/luci-static/resources/view/fchomo/node.js:1608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1614 +#: htdocs/luci-static/resources/view/fchomo/node.js:1628 +#: htdocs/luci-static/resources/view/fchomo/node.js:1634 msgid "-- Please choose --" msgstr "" @@ -63,15 +62,15 @@ msgstr "" msgid "2022-blake3-chacha20-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:617 +#: htdocs/luci-static/resources/view/fchomo/client.js:627 msgid "0 or 1 only." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:897 -#: htdocs/luci-static/resources/view/fchomo/node.js:911 -#: htdocs/luci-static/resources/view/fchomo/server.js:800 -#: htdocs/luci-static/resources/view/fchomo/server.js:815 -#: htdocs/luci-static/resources/view/fchomo/server.js:840 +#: htdocs/luci-static/resources/view/fchomo/node.js:918 +#: htdocs/luci-static/resources/view/fchomo/node.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:786 +#: htdocs/luci-static/resources/view/fchomo/server.js:801 +#: htdocs/luci-static/resources/view/fchomo/server.js:826 msgid "Save your configuration before uploading files!" msgstr "" @@ -128,56 +127,56 @@ msgstr "" msgid "Access Control" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1551 +#: htdocs/luci-static/resources/view/fchomo/client.js:1545 msgid "Add a DNS policy" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1314 +#: htdocs/luci-static/resources/view/fchomo/client.js:1312 msgid "Add a DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:215 +#: htdocs/luci-static/resources/view/fchomo/node.js:217 msgid "Add a Node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 +#: htdocs/luci-static/resources/view/fchomo/node.js:1198 msgid "Add a provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1567 +#: htdocs/luci-static/resources/view/fchomo/node.js:1584 msgid "Add a proxy chain" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:819 +#: htdocs/luci-static/resources/view/fchomo/client.js:829 msgid "Add a proxy group" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:1081 msgid "Add a routing rule" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:133 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:135 msgid "Add a rule set" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 +#: htdocs/luci-static/resources/view/fchomo/server.js:166 msgid "Add a server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1185 +#: htdocs/luci-static/resources/view/fchomo/client.js:1187 msgid "Add a sub rule" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1392 +#: htdocs/luci-static/resources/view/fchomo/node.js:1409 msgid "Add prefix" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1396 +#: htdocs/luci-static/resources/view/fchomo/node.js:1413 msgid "Add suffix" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1376 -#: htdocs/luci-static/resources/view/fchomo/client.js:1381 +#: htdocs/luci-static/resources/view/fchomo/client.js:1370 +#: htdocs/luci-static/resources/view/fchomo/client.js:1375 msgid "Address" msgstr "" @@ -193,7 +192,7 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:779 #: htdocs/luci-static/resources/view/fchomo/global.js:842 -#: htdocs/luci-static/resources/view/fchomo/global.js:861 +#: htdocs/luci-static/resources/view/fchomo/global.js:876 msgid "All allowed" msgstr "" @@ -207,7 +206,7 @@ msgid "" "network from a public website, it must be enabled." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:630 +#: htdocs/luci-static/resources/view/fchomo/node.js:651 msgid "Allowed IPs" msgstr "" @@ -219,8 +218,8 @@ msgstr "" msgid "Already in updating." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:559 -#: htdocs/luci-static/resources/view/fchomo/server.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:580 +#: htdocs/luci-static/resources/view/fchomo/server.js:488 msgid "Alter ID" msgstr "" @@ -242,11 +241,11 @@ msgstr "" msgid "As the TOP upstream of dnsmasq." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:452 +#: htdocs/luci-static/resources/view/fchomo/server.js:438 msgid "Auth timeout" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:581 +#: htdocs/luci-static/resources/view/fchomo/node.js:602 msgid "Authenticated length" msgstr "" @@ -254,7 +253,7 @@ msgstr "" msgid "Auto" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:222 +#: htdocs/luci-static/resources/view/fchomo/server.js:189 msgid "Auto configure firewall" msgstr "" @@ -274,27 +273,27 @@ msgstr "" msgid "Based on google/gvisor." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:269 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:267 msgid "Behavior" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:278 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:292 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:276 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:290 msgid "Binary format only supports domain / ipcidr" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:286 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 msgid "Binary mrs" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:740 -#: htdocs/luci-static/resources/view/fchomo/node.js:1147 -#: htdocs/luci-static/resources/view/fchomo/node.js:1465 +#: htdocs/luci-static/resources/view/fchomo/node.js:1168 +#: htdocs/luci-static/resources/view/fchomo/node.js:1482 msgid "Bind interface" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1148 -#: htdocs/luci-static/resources/view/fchomo/node.js:1466 +#: htdocs/luci-static/resources/view/fchomo/node.js:1169 +#: htdocs/luci-static/resources/view/fchomo/node.js:1483 msgid "Bind outbound interface.
" msgstr "" @@ -307,15 +306,15 @@ msgstr "" msgid "Black list" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:55 +#: htdocs/luci-static/resources/view/fchomo/client.js:57 msgid "Block DNS queries" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1259 +#: htdocs/luci-static/resources/view/fchomo/client.js:1257 msgid "Bootstrap DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1266 +#: htdocs/luci-static/resources/view/fchomo/client.js:1264 msgid "Bootstrap DNS server (Node)" msgstr "" @@ -323,7 +322,7 @@ msgstr "" msgid "Bypass CN" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:862 +#: htdocs/luci-static/resources/view/fchomo/global.js:877 msgid "Bypass DSCP" msgstr "" @@ -339,21 +338,21 @@ msgstr "" msgid "CORS allowed origins, * will be used if empty." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:614 +#: htdocs/luci-static/resources/fchomo.js:674 msgid "Cancel" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:869 +#: htdocs/luci-static/resources/view/fchomo/node.js:890 msgid "Cert fingerprint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:870 +#: htdocs/luci-static/resources/view/fchomo/node.js:891 msgid "" "Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:890 -#: htdocs/luci-static/resources/view/fchomo/server.js:792 +#: htdocs/luci-static/resources/view/fchomo/node.js:911 +#: htdocs/luci-static/resources/view/fchomo/server.js:778 msgid "Certificate path" msgstr "" @@ -382,14 +381,19 @@ msgstr "" msgid "China list version" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:324 -#: htdocs/luci-static/resources/view/fchomo/node.js:377 -#: htdocs/luci-static/resources/view/fchomo/node.js:565 -#: htdocs/luci-static/resources/view/fchomo/server.js:302 -#: htdocs/luci-static/resources/view/fchomo/server.js:389 +#: htdocs/luci-static/resources/view/fchomo/node.js:326 +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +#: htdocs/luci-static/resources/view/fchomo/node.js:586 +#: htdocs/luci-static/resources/view/fchomo/server.js:269 +#: htdocs/luci-static/resources/view/fchomo/server.js:356 msgid "Chipher" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/node.js:388 +#: htdocs/luci-static/resources/view/fchomo/server.js:365 +msgid "Chipher must be enabled if obfuscate downlink is disabled." +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/log.js:119 msgid "Clean log" msgstr "" @@ -401,28 +405,28 @@ msgid "" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:556 -#: htdocs/luci-static/resources/view/fchomo/node.js:767 -#: htdocs/luci-static/resources/view/fchomo/node.js:891 -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:621 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/view/fchomo/node.js:788 +#: htdocs/luci-static/resources/view/fchomo/node.js:912 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 +#: htdocs/luci-static/resources/view/fchomo/server.js:607 +#: htdocs/luci-static/resources/view/fchomo/server.js:817 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 msgid "Client" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:830 +#: htdocs/luci-static/resources/view/fchomo/server.js:816 msgid "Client Auth Certificate path" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:822 +#: htdocs/luci-static/resources/view/fchomo/server.js:808 msgid "Client Auth type" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/node.js:952 msgid "Client fingerprint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:385 +#: htdocs/luci-static/resources/view/fchomo/server.js:352 msgid "Client key" msgstr "" @@ -439,16 +443,16 @@ msgstr "" msgid "Common ports (bypass P2P traffic)" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1195 +#: htdocs/luci-static/resources/fchomo.js:1245 msgid "Complete" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1412 +#: htdocs/luci-static/resources/view/fchomo/node.js:1429 msgid "Configuration Items" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:439 -#: htdocs/luci-static/resources/view/fchomo/server.js:430 +#: htdocs/luci-static/resources/view/fchomo/node.js:460 +#: htdocs/luci-static/resources/view/fchomo/server.js:416 msgid "Congestion controller" msgstr "" @@ -456,20 +460,20 @@ msgstr "" msgid "Connection check" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:35 +#: htdocs/luci-static/resources/fchomo.js:543 msgid "Content copied to clipboard!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:598 -#: htdocs/luci-static/resources/view/fchomo/node.js:1322 -#: htdocs/luci-static/resources/view/fchomo/node.js:1348 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:326 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:352 +#: htdocs/luci-static/resources/view/fchomo/client.js:608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 +#: htdocs/luci-static/resources/view/fchomo/node.js:1365 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:324 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:350 msgid "Content will not be verified, Please make sure you enter it correctly." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1321 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:325 +#: htdocs/luci-static/resources/view/fchomo/node.js:1338 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:323 msgid "Contents" msgstr "" @@ -477,7 +481,7 @@ msgstr "" msgid "Contents have been saved." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:37 +#: htdocs/luci-static/resources/fchomo.js:545 msgid "Copy" msgstr "" @@ -489,18 +493,23 @@ msgstr "" msgid "Cron expression" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:880 +#: htdocs/luci-static/resources/view/fchomo/global.js:895 msgid "Custom Direct List" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1383 +#: htdocs/luci-static/resources/view/fchomo/node.js:1400 msgid "Custom HTTP header." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:898 +#: htdocs/luci-static/resources/view/fchomo/global.js:913 msgid "Custom Proxy List" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/node.js:401 +#: htdocs/luci-static/resources/view/fchomo/server.js:378 +msgid "Custom byte layout" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/hosts.js:28 msgid "" "Custom internal hosts. Support yaml or json format." @@ -510,8 +519,8 @@ msgstr "" msgid "DIRECT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1542 -#: htdocs/luci-static/resources/view/fchomo/client.js:1551 +#: htdocs/luci-static/resources/view/fchomo/client.js:1536 +#: htdocs/luci-static/resources/view/fchomo/client.js:1545 msgid "DNS policy" msgstr "" @@ -519,18 +528,18 @@ msgstr "" msgid "DNS port" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1305 -#: htdocs/luci-static/resources/view/fchomo/client.js:1314 -#: htdocs/luci-static/resources/view/fchomo/client.js:1635 -#: htdocs/luci-static/resources/view/fchomo/node.js:654 +#: htdocs/luci-static/resources/view/fchomo/client.js:1303 +#: htdocs/luci-static/resources/view/fchomo/client.js:1312 +#: htdocs/luci-static/resources/view/fchomo/client.js:1625 +#: htdocs/luci-static/resources/view/fchomo/node.js:675 msgid "DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1245 +#: htdocs/luci-static/resources/view/fchomo/client.js:1243 msgid "DNS settings" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:865 +#: htdocs/luci-static/resources/view/fchomo/global.js:880 msgid "DSCP list" msgstr "" @@ -542,32 +551,32 @@ msgstr "" msgid "Debug" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:53 +#: htdocs/luci-static/resources/view/fchomo/client.js:55 msgid "Default DNS (issued by WAN)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1280 +#: htdocs/luci-static/resources/view/fchomo/client.js:1278 msgid "Default DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:631 +#: htdocs/luci-static/resources/view/fchomo/node.js:652 msgid "Destination addresses allowed to be forwarded via Wireguard." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1590 +#: htdocs/luci-static/resources/view/fchomo/node.js:1607 msgid "Destination provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1596 +#: htdocs/luci-static/resources/view/fchomo/node.js:1613 msgid "Destination proxy node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:225 +#: htdocs/luci-static/resources/view/fchomo/node.js:227 msgid "Dial fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1603 -#: htdocs/luci-static/resources/view/fchomo/node.js:1623 +#: htdocs/luci-static/resources/view/fchomo/node.js:1620 +#: htdocs/luci-static/resources/view/fchomo/node.js:1640 msgid "Different chain head/tail" msgstr "" @@ -588,8 +597,8 @@ msgid "Direct MAC-s" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:403 -#: htdocs/luci-static/resources/view/fchomo/node.js:290 -#: htdocs/luci-static/resources/view/fchomo/server.js:282 +#: htdocs/luci-static/resources/view/fchomo/node.js:292 +#: htdocs/luci-static/resources/view/fchomo/server.js:249 msgid "Disable" msgstr "" @@ -605,11 +614,11 @@ msgstr "" msgid "Disable ICMP Forwarding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:818 +#: htdocs/luci-static/resources/view/fchomo/node.js:839 msgid "Disable SNI" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:967 +#: htdocs/luci-static/resources/view/fchomo/client.js:973 msgid "Disable UDP" msgstr "" @@ -617,51 +626,51 @@ msgstr "" msgid "Disable safe path check" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1508 +#: htdocs/luci-static/resources/view/fchomo/client.js:1502 msgid "Discard A responses" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1524 +#: htdocs/luci-static/resources/view/fchomo/client.js:1518 msgid "Discard AAAA responses" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:750 +#: htdocs/luci-static/resources/view/fchomo/client.js:760 msgid "" "Do not resolve the domain connection to IP for this match.
Only works " "for pure domain inbound connections without DNS resolution. e.g., socks5h" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1605 -#: htdocs/luci-static/resources/view/fchomo/client.js:1610 -#: htdocs/luci-static/resources/view/fchomo/client.js:1675 -#: htdocs/luci-static/resources/view/fchomo/client.js:1682 -#: htdocs/luci-static/resources/view/fchomo/client.js:1684 +#: htdocs/luci-static/resources/view/fchomo/client.js:1595 +#: htdocs/luci-static/resources/view/fchomo/client.js:1600 +#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1672 +#: htdocs/luci-static/resources/view/fchomo/client.js:1674 msgid "Domain" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:840 msgid "Donot send server name in ClientHello." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1456 -#: htdocs/luci-static/resources/view/fchomo/node.js:883 -#: htdocs/luci-static/resources/view/fchomo/node.js:1452 +#: htdocs/luci-static/resources/view/fchomo/client.js:1450 +#: htdocs/luci-static/resources/view/fchomo/node.js:904 +#: htdocs/luci-static/resources/view/fchomo/node.js:1469 msgid "Donot verifying server certificate." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1147 msgid "Download bandwidth" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1127 +#: htdocs/luci-static/resources/view/fchomo/node.js:1148 msgid "Download bandwidth in Mbps." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1080 +#: htdocs/luci-static/resources/fchomo.js:1130 msgid "Download failed: %s" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1078 +#: htdocs/luci-static/resources/fchomo.js:1128 msgid "Download successful." msgstr "" @@ -669,20 +678,20 @@ msgstr "" msgid "Dual stack" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:924 -#: htdocs/luci-static/resources/view/fchomo/server.js:888 +#: htdocs/luci-static/resources/view/fchomo/node.js:945 +#: htdocs/luci-static/resources/view/fchomo/server.js:874 msgid "ECH config" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:847 +#: htdocs/luci-static/resources/view/fchomo/server.js:833 msgid "ECH key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1490 +#: htdocs/luci-static/resources/view/fchomo/client.js:1484 msgid "ECS override" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1474 +#: htdocs/luci-static/resources/view/fchomo/client.js:1468 msgid "EDNS Client Subnet" msgstr "" @@ -690,24 +699,24 @@ msgstr "" msgid "ETag support" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1039 +#: htdocs/luci-static/resources/view/fchomo/node.js:1060 msgid "Early Data first packet length limit." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1045 +#: htdocs/luci-static/resources/view/fchomo/node.js:1066 msgid "Early Data header name" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:201 +#: htdocs/luci-static/resources/view/fchomo/node.js:203 msgid "Edit node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:124 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:126 msgid "Edit ruleset" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1319 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:323 +#: htdocs/luci-static/resources/view/fchomo/node.js:1336 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:321 msgid "Editer" msgstr "" @@ -715,32 +724,32 @@ msgstr "" msgid "Eliminate encryption header characteristics" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:809 -#: htdocs/luci-static/resources/view/fchomo/client.js:906 -#: htdocs/luci-static/resources/view/fchomo/client.js:1135 -#: htdocs/luci-static/resources/view/fchomo/client.js:1231 -#: htdocs/luci-static/resources/view/fchomo/client.js:1372 -#: htdocs/luci-static/resources/view/fchomo/client.js:1594 +#: htdocs/luci-static/resources/view/fchomo/client.js:819 +#: htdocs/luci-static/resources/view/fchomo/client.js:912 +#: htdocs/luci-static/resources/view/fchomo/client.js:1137 +#: htdocs/luci-static/resources/view/fchomo/client.js:1229 +#: htdocs/luci-static/resources/view/fchomo/client.js:1366 +#: htdocs/luci-static/resources/view/fchomo/client.js:1584 #: htdocs/luci-static/resources/view/fchomo/global.js:401 #: htdocs/luci-static/resources/view/fchomo/global.js:686 -#: htdocs/luci-static/resources/view/fchomo/node.js:232 -#: htdocs/luci-static/resources/view/fchomo/node.js:1292 -#: htdocs/luci-static/resources/view/fchomo/node.js:1488 -#: htdocs/luci-static/resources/view/fchomo/node.js:1577 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:251 -#: htdocs/luci-static/resources/view/fchomo/server.js:190 -#: htdocs/luci-static/resources/view/fchomo/server.js:217 +#: htdocs/luci-static/resources/view/fchomo/node.js:234 +#: htdocs/luci-static/resources/view/fchomo/node.js:1309 +#: htdocs/luci-static/resources/view/fchomo/node.js:1505 +#: htdocs/luci-static/resources/view/fchomo/node.js:1594 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:249 +#: htdocs/luci-static/resources/view/fchomo/server.js:157 +#: htdocs/luci-static/resources/view/fchomo/server.js:184 msgid "Enable" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:495 msgid "" "Enable 0-RTT QUIC connection handshake on the client side. This is not " "impacting much on the performance, as the protocol is fully multiplexed.
Disabling this is highly recommended, as it is vulnerable to replay attacks." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:494 msgid "Enable 0-RTT handshake" msgstr "" @@ -750,44 +759,49 @@ msgid "" "conversion for outbound connections" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:918 +#: htdocs/luci-static/resources/view/fchomo/node.js:939 msgid "Enable ECH" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1114 +#: htdocs/luci-static/resources/view/fchomo/node.js:1135 msgid "Enable TCP Brutal" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1115 +#: htdocs/luci-static/resources/view/fchomo/node.js:1136 msgid "Enable TCP Brutal congestion control algorithm" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1103 +#: htdocs/luci-static/resources/view/fchomo/node.js:1124 msgid "Enable multiplexing only for TCP." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1097 +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +#: htdocs/luci-static/resources/view/fchomo/server.js:402 +msgid "Enable obfuscate for downlink" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1118 msgid "Enable padding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1108 +#: htdocs/luci-static/resources/view/fchomo/node.js:1129 msgid "Enable statistic" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:719 -#: htdocs/luci-static/resources/view/fchomo/node.js:1434 +#: htdocs/luci-static/resources/view/fchomo/node.js:740 +#: htdocs/luci-static/resources/view/fchomo/node.js:1451 msgid "" "Enable the SUoT protocol, requires server support. Conflict with Multiplex." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:296 -#: htdocs/luci-static/resources/view/fchomo/server.js:288 +#: htdocs/luci-static/resources/view/fchomo/node.js:298 +#: htdocs/luci-static/resources/view/fchomo/server.js:255 msgid "" "Enabling obfuscation will make the server incompatible with standard QUIC " "connections, losing the ability to masquerade with HTTP/3." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:584 +#: htdocs/luci-static/resources/view/fchomo/server.js:570 msgid "Encryption method" msgstr "" @@ -795,8 +809,8 @@ msgstr "" msgid "Endpoint-Independent NAT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:650 -#: htdocs/luci-static/resources/view/fchomo/client.js:1627 +#: htdocs/luci-static/resources/view/fchomo/client.js:660 +#: htdocs/luci-static/resources/view/fchomo/client.js:1617 msgid "Entry" msgstr "" @@ -804,24 +818,24 @@ msgstr "" msgid "Error" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1011 +#: htdocs/luci-static/resources/view/fchomo/client.js:1017 msgid "" "Exceeding this triggers a forced health check. 5 will be used " "if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1546 +#: htdocs/luci-static/resources/view/fchomo/node.js:1563 msgid "Exclude matched node types." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +#: htdocs/luci-static/resources/view/fchomo/client.js:1054 msgid "" "Exclude matched node types. Available types see here." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1043 -#: htdocs/luci-static/resources/view/fchomo/node.js:1539 +#: htdocs/luci-static/resources/view/fchomo/client.js:1049 +#: htdocs/luci-static/resources/view/fchomo/node.js:1556 msgid "Exclude nodes that meet keywords or regexps." msgstr "" @@ -829,58 +843,60 @@ msgstr "" msgid "Expand/Collapse result" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1003 -#: htdocs/luci-static/resources/view/fchomo/node.js:1524 +#: htdocs/luci-static/resources/view/fchomo/client.js:1009 +#: htdocs/luci-static/resources/view/fchomo/node.js:1541 msgid "Expected HTTP code. 204 will be used if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1005 -#: htdocs/luci-static/resources/view/fchomo/node.js:1526 +#: htdocs/luci-static/resources/view/fchomo/client.js:1011 +#: htdocs/luci-static/resources/view/fchomo/node.js:1543 msgid "Expected status" msgstr "" #: htdocs/luci-static/resources/fchomo.js:393 #: htdocs/luci-static/resources/fchomo.js:396 #: htdocs/luci-static/resources/fchomo.js:399 -#: htdocs/luci-static/resources/fchomo.js:1212 -#: htdocs/luci-static/resources/fchomo.js:1220 -#: htdocs/luci-static/resources/fchomo.js:1228 -#: htdocs/luci-static/resources/fchomo.js:1251 -#: htdocs/luci-static/resources/fchomo.js:1254 -#: htdocs/luci-static/resources/fchomo.js:1261 -#: htdocs/luci-static/resources/fchomo.js:1277 -#: htdocs/luci-static/resources/fchomo.js:1286 -#: htdocs/luci-static/resources/fchomo.js:1298 +#: htdocs/luci-static/resources/fchomo.js:1262 +#: htdocs/luci-static/resources/fchomo.js:1270 +#: htdocs/luci-static/resources/fchomo.js:1278 #: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1304 #: htdocs/luci-static/resources/fchomo.js:1311 -#: htdocs/luci-static/resources/fchomo.js:1323 -#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1327 #: htdocs/luci-static/resources/fchomo.js:1336 -#: htdocs/luci-static/resources/fchomo.js:1346 -#: htdocs/luci-static/resources/fchomo.js:1381 -#: htdocs/luci-static/resources/fchomo.js:1383 -#: htdocs/luci-static/resources/fchomo.js:1393 -#: htdocs/luci-static/resources/fchomo.js:1402 -#: htdocs/luci-static/resources/view/fchomo/client.js:66 -#: htdocs/luci-static/resources/view/fchomo/client.js:900 -#: htdocs/luci-static/resources/view/fchomo/client.js:1387 -#: htdocs/luci-static/resources/view/fchomo/global.js:871 -#: htdocs/luci-static/resources/view/fchomo/node.js:790 -#: htdocs/luci-static/resources/view/fchomo/node.js:875 -#: htdocs/luci-static/resources/view/fchomo/node.js:1603 -#: htdocs/luci-static/resources/view/fchomo/node.js:1623 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:278 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:292 -#: htdocs/luci-static/resources/view/fchomo/server.js:613 -#: htdocs/luci-static/resources/view/fchomo/server.js:644 -#: htdocs/luci-static/resources/view/fchomo/server.js:745 +#: htdocs/luci-static/resources/fchomo.js:1348 +#: htdocs/luci-static/resources/fchomo.js:1351 +#: htdocs/luci-static/resources/fchomo.js:1361 +#: htdocs/luci-static/resources/fchomo.js:1373 +#: htdocs/luci-static/resources/fchomo.js:1376 +#: htdocs/luci-static/resources/fchomo.js:1386 +#: htdocs/luci-static/resources/fchomo.js:1396 +#: htdocs/luci-static/resources/fchomo.js:1431 +#: htdocs/luci-static/resources/fchomo.js:1433 +#: htdocs/luci-static/resources/fchomo.js:1443 +#: htdocs/luci-static/resources/fchomo.js:1452 +#: htdocs/luci-static/resources/view/fchomo/client.js:68 +#: htdocs/luci-static/resources/view/fchomo/client.js:906 +#: htdocs/luci-static/resources/view/fchomo/client.js:1381 +#: htdocs/luci-static/resources/view/fchomo/global.js:886 +#: htdocs/luci-static/resources/view/fchomo/node.js:388 +#: htdocs/luci-static/resources/view/fchomo/node.js:811 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 +#: htdocs/luci-static/resources/view/fchomo/node.js:1620 +#: htdocs/luci-static/resources/view/fchomo/node.js:1640 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:276 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:290 +#: htdocs/luci-static/resources/view/fchomo/server.js:365 +#: htdocs/luci-static/resources/view/fchomo/server.js:599 +#: htdocs/luci-static/resources/view/fchomo/server.js:630 +#: htdocs/luci-static/resources/view/fchomo/server.js:731 msgid "Expecting: %s" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:980 -#: htdocs/luci-static/resources/view/fchomo/node.js:987 -#: htdocs/luci-static/resources/view/fchomo/server.js:955 -#: htdocs/luci-static/resources/view/fchomo/server.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:1001 +#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/server.js:941 +#: htdocs/luci-static/resources/view/fchomo/server.js:948 msgid "Expecting: only support %s." msgstr "" @@ -888,30 +904,30 @@ msgstr "" msgid "Experimental" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:478 -#: htdocs/luci-static/resources/view/fchomo/client.js:491 -#: htdocs/luci-static/resources/view/fchomo/client.js:498 +#: htdocs/luci-static/resources/view/fchomo/client.js:488 +#: htdocs/luci-static/resources/view/fchomo/client.js:501 #: htdocs/luci-static/resources/view/fchomo/client.js:508 -#: htdocs/luci-static/resources/view/fchomo/client.js:515 -#: htdocs/luci-static/resources/view/fchomo/client.js:523 -#: htdocs/luci-static/resources/view/fchomo/client.js:530 -#: htdocs/luci-static/resources/view/fchomo/client.js:597 +#: htdocs/luci-static/resources/view/fchomo/client.js:518 +#: htdocs/luci-static/resources/view/fchomo/client.js:525 +#: htdocs/luci-static/resources/view/fchomo/client.js:533 +#: htdocs/luci-static/resources/view/fchomo/client.js:540 +#: htdocs/luci-static/resources/view/fchomo/client.js:607 msgid "Factor" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1153 +#: htdocs/luci-static/resources/fchomo.js:1203 msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1112 +#: htdocs/luci-static/resources/fchomo.js:1162 msgid "Failed to generate %s, error: %s." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1502 +#: htdocs/luci-static/resources/fchomo.js:1552 msgid "Failed to upload %s, error: %s." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1521 +#: htdocs/luci-static/resources/fchomo.js:1571 msgid "Failed to upload, error: %s." msgstr "" @@ -919,56 +935,56 @@ msgstr "" msgid "Fallback" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1274 -#: htdocs/luci-static/resources/view/fchomo/client.js:1287 +#: htdocs/luci-static/resources/view/fchomo/client.js:1272 +#: htdocs/luci-static/resources/view/fchomo/client.js:1285 msgid "Fallback DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1654 +#: htdocs/luci-static/resources/view/fchomo/client.js:1644 msgid "Fallback filter" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1038 -#: htdocs/luci-static/resources/view/fchomo/node.js:1533 +#: htdocs/luci-static/resources/view/fchomo/client.js:1044 +#: htdocs/luci-static/resources/view/fchomo/node.js:1550 msgid "Filter nodes that meet keywords or regexps." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1273 +#: htdocs/luci-static/resources/view/fchomo/client.js:1271 msgid "Final DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1275 +#: htdocs/luci-static/resources/view/fchomo/client.js:1273 msgid "Final DNS server (For non-poisoned domains)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1277 +#: htdocs/luci-static/resources/view/fchomo/client.js:1275 msgid "Final DNS server (For poisoned domains)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:188 msgid "Firewall" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:551 -#: htdocs/luci-static/resources/view/fchomo/server.js:494 +#: htdocs/luci-static/resources/view/fchomo/node.js:572 +#: htdocs/luci-static/resources/view/fchomo/server.js:480 msgid "Flow" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1027 +#: htdocs/luci-static/resources/view/fchomo/client.js:1033 msgid "" "For details, see %s." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1004 -#: htdocs/luci-static/resources/view/fchomo/node.js:1402 -#: htdocs/luci-static/resources/view/fchomo/node.js:1525 +#: htdocs/luci-static/resources/view/fchomo/client.js:1010 +#: htdocs/luci-static/resources/view/fchomo/node.js:1419 +#: htdocs/luci-static/resources/view/fchomo/node.js:1542 msgid "" "For format see %s." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:649 +#: htdocs/luci-static/resources/view/fchomo/node.js:670 msgid "Force DNS remote resolution." msgstr "" @@ -976,7 +992,7 @@ msgstr "" msgid "Forced sniffing domain" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:283 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:281 msgid "Format" msgstr "" @@ -987,7 +1003,7 @@ msgstr "" msgid "FullCombo Shark!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1006 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 msgid "GET" msgstr "" @@ -999,10 +1015,10 @@ msgstr "" msgid "General" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:891 -#: htdocs/luci-static/resources/view/fchomo/node.js:220 -#: htdocs/luci-static/resources/view/fchomo/node.js:1282 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 +#: htdocs/luci-static/resources/view/fchomo/client.js:897 +#: htdocs/luci-static/resources/view/fchomo/node.js:222 +#: htdocs/luci-static/resources/view/fchomo/node.js:1299 +#: htdocs/luci-static/resources/view/fchomo/server.js:171 msgid "General fields" msgstr "" @@ -1015,9 +1031,9 @@ msgstr "" #: htdocs/luci-static/resources/fchomo.js:510 #: htdocs/luci-static/resources/fchomo.js:512 #: htdocs/luci-static/resources/view/fchomo/global.js:593 -#: htdocs/luci-static/resources/view/fchomo/server.js:376 -#: htdocs/luci-static/resources/view/fchomo/server.js:717 -#: htdocs/luci-static/resources/view/fchomo/server.js:880 +#: htdocs/luci-static/resources/view/fchomo/server.js:343 +#: htdocs/luci-static/resources/view/fchomo/server.js:703 +#: htdocs/luci-static/resources/view/fchomo/server.js:866 msgid "Generate" msgstr "" @@ -1033,17 +1049,17 @@ msgstr "" msgid "GeoSite version" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1664 +#: htdocs/luci-static/resources/view/fchomo/client.js:1654 msgid "Geoip code" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1661 +#: htdocs/luci-static/resources/view/fchomo/client.js:1651 msgid "Geoip enable" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1606 -#: htdocs/luci-static/resources/view/fchomo/client.js:1615 -#: htdocs/luci-static/resources/view/fchomo/client.js:1673 +#: htdocs/luci-static/resources/view/fchomo/client.js:1596 +#: htdocs/luci-static/resources/view/fchomo/client.js:1605 +#: htdocs/luci-static/resources/view/fchomo/client.js:1663 msgid "Geosite" msgstr "" @@ -1064,7 +1080,7 @@ msgstr "" msgid "Global client fingerprint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:575 +#: htdocs/luci-static/resources/view/fchomo/node.js:596 msgid "Global padding" msgstr "" @@ -1080,107 +1096,107 @@ msgstr "" msgid "Grant access to fchomo configuration" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:916 +#: htdocs/luci-static/resources/view/fchomo/client.js:922 msgid "Group" msgstr "" #: htdocs/luci-static/resources/fchomo.js:126 #: htdocs/luci-static/resources/fchomo.js:158 -#: htdocs/luci-static/resources/view/fchomo/node.js:672 -#: htdocs/luci-static/resources/view/fchomo/node.js:969 -#: htdocs/luci-static/resources/view/fchomo/node.js:980 -#: htdocs/luci-static/resources/view/fchomo/server.js:955 +#: htdocs/luci-static/resources/view/fchomo/node.js:693 +#: htdocs/luci-static/resources/view/fchomo/node.js:990 +#: htdocs/luci-static/resources/view/fchomo/node.js:1001 +#: htdocs/luci-static/resources/view/fchomo/server.js:941 msgid "HTTP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:265 -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/node.js:1382 +#: htdocs/luci-static/resources/view/fchomo/node.js:267 +#: htdocs/luci-static/resources/view/fchomo/node.js:1049 +#: htdocs/luci-static/resources/view/fchomo/node.js:1399 msgid "HTTP header" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:405 +#: htdocs/luci-static/resources/view/fchomo/node.js:419 msgid "HTTP mask" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1005 +#: htdocs/luci-static/resources/view/fchomo/node.js:1026 msgid "HTTP request method" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1439 +#: htdocs/luci-static/resources/view/fchomo/client.js:1433 msgid "HTTP/3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:296 +#: htdocs/luci-static/resources/view/fchomo/server.js:263 msgid "" "HTTP3 server behavior when authentication fails.
A 404 page will be " "returned if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:970 -#: htdocs/luci-static/resources/view/fchomo/node.js:981 -#: htdocs/luci-static/resources/view/fchomo/server.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:991 +#: htdocs/luci-static/resources/view/fchomo/node.js:1002 +#: htdocs/luci-static/resources/view/fchomo/server.js:942 msgid "HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:846 +#: htdocs/luci-static/resources/view/fchomo/global.js:861 msgid "Handle domain" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:363 +#: htdocs/luci-static/resources/view/fchomo/node.js:365 msgid "Handshake mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:517 +#: htdocs/luci-static/resources/view/fchomo/server.js:503 msgid "Handshake target that supports TLS 1.3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:417 +#: htdocs/luci-static/resources/view/fchomo/server.js:396 msgid "Handshake timeout" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:973 -#: htdocs/luci-static/resources/view/fchomo/node.js:1493 +#: htdocs/luci-static/resources/view/fchomo/client.js:979 +#: htdocs/luci-static/resources/view/fchomo/node.js:1510 msgid "Health check URL" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1002 -#: htdocs/luci-static/resources/view/fchomo/node.js:1523 +#: htdocs/luci-static/resources/view/fchomo/client.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1540 msgid "Health check expected status" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:982 -#: htdocs/luci-static/resources/view/fchomo/node.js:1503 +#: htdocs/luci-static/resources/view/fchomo/client.js:988 +#: htdocs/luci-static/resources/view/fchomo/node.js:1520 msgid "Health check interval" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:989 -#: htdocs/luci-static/resources/view/fchomo/node.js:1510 +#: htdocs/luci-static/resources/view/fchomo/client.js:995 +#: htdocs/luci-static/resources/view/fchomo/node.js:1527 msgid "Health check timeout" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:893 -#: htdocs/luci-static/resources/view/fchomo/node.js:1284 +#: htdocs/luci-static/resources/view/fchomo/client.js:899 +#: htdocs/luci-static/resources/view/fchomo/node.js:1301 msgid "Health fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:480 +#: htdocs/luci-static/resources/view/fchomo/node.js:501 msgid "Heartbeat interval" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1053 +#: htdocs/luci-static/resources/view/fchomo/client.js:1059 msgid "Hidden" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:678 +#: htdocs/luci-static/resources/view/fchomo/node.js:699 msgid "Host that supports TLS 1.3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:318 +#: htdocs/luci-static/resources/view/fchomo/node.js:320 msgid "Host-key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:313 +#: htdocs/luci-static/resources/view/fchomo/node.js:315 msgid "Host-key algorithms" msgstr "" @@ -1194,21 +1210,21 @@ msgstr "" msgid "Hysteria2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1666 -#: htdocs/luci-static/resources/view/fchomo/client.js:1679 +#: htdocs/luci-static/resources/view/fchomo/client.js:1656 +#: htdocs/luci-static/resources/view/fchomo/client.js:1669 msgid "IP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1677 +#: htdocs/luci-static/resources/view/fchomo/client.js:1667 msgid "IP CIDR" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 msgid "IP override" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1159 -#: htdocs/luci-static/resources/view/fchomo/node.js:1479 +#: htdocs/luci-static/resources/view/fchomo/node.js:1180 +#: htdocs/luci-static/resources/view/fchomo/node.js:1496 msgid "IP version" msgstr "" @@ -1220,110 +1236,110 @@ msgstr "" msgid "IPv6 only" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1256 +#: htdocs/luci-static/resources/view/fchomo/client.js:1254 #: htdocs/luci-static/resources/view/fchomo/global.js:415 msgid "IPv6 support" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1059 +#: htdocs/luci-static/resources/view/fchomo/client.js:1065 msgid "Icon" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:524 +#: htdocs/luci-static/resources/view/fchomo/node.js:545 msgid "Idle session check interval" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:531 +#: htdocs/luci-static/resources/view/fchomo/node.js:552 msgid "Idle session timeout" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:445 +#: htdocs/luci-static/resources/view/fchomo/server.js:431 msgid "Idle timeout" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1254 +#: htdocs/luci-static/resources/fchomo.js:1304 msgid "If All ports is selected, uncheck others" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:66 +#: htdocs/luci-static/resources/view/fchomo/client.js:68 msgid "If Block is selected, uncheck others" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:275 +#: htdocs/luci-static/resources/view/fchomo/server.js:242 msgid "Ignore client bandwidth" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:619 +#: htdocs/luci-static/resources/fchomo.js:679 msgid "Import" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:826 -#: htdocs/luci-static/resources/view/fchomo/client.js:885 -#: htdocs/luci-static/resources/view/fchomo/client.js:1082 -#: htdocs/luci-static/resources/view/fchomo/client.js:1124 -#: htdocs/luci-static/resources/view/fchomo/client.js:1192 -#: htdocs/luci-static/resources/view/fchomo/client.js:1220 -#: htdocs/luci-static/resources/view/fchomo/client.js:1321 -#: htdocs/luci-static/resources/view/fchomo/client.js:1361 -#: htdocs/luci-static/resources/view/fchomo/client.js:1558 -#: htdocs/luci-static/resources/view/fchomo/client.js:1583 -#: htdocs/luci-static/resources/view/fchomo/node.js:1184 -#: htdocs/luci-static/resources/view/fchomo/node.js:1270 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:140 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:228 +#: htdocs/luci-static/resources/view/fchomo/client.js:836 +#: htdocs/luci-static/resources/view/fchomo/client.js:891 +#: htdocs/luci-static/resources/view/fchomo/client.js:1088 +#: htdocs/luci-static/resources/view/fchomo/client.js:1126 +#: htdocs/luci-static/resources/view/fchomo/client.js:1194 +#: htdocs/luci-static/resources/view/fchomo/client.js:1218 +#: htdocs/luci-static/resources/view/fchomo/client.js:1319 +#: htdocs/luci-static/resources/view/fchomo/client.js:1355 +#: htdocs/luci-static/resources/view/fchomo/client.js:1552 +#: htdocs/luci-static/resources/view/fchomo/client.js:1573 +#: htdocs/luci-static/resources/view/fchomo/node.js:1205 +#: htdocs/luci-static/resources/view/fchomo/node.js:1287 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:142 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:226 msgid "Import mihomo config" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:181 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:179 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:230 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:234 msgid "Import rule-set links" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:278 -#: htdocs/luci-static/resources/view/fchomo/node.js:284 -#: htdocs/luci-static/resources/view/fchomo/node.js:1440 -#: htdocs/luci-static/resources/view/fchomo/node.js:1446 -#: htdocs/luci-static/resources/view/fchomo/server.js:264 -#: htdocs/luci-static/resources/view/fchomo/server.js:270 +#: htdocs/luci-static/resources/view/fchomo/node.js:280 +#: htdocs/luci-static/resources/view/fchomo/node.js:286 +#: htdocs/luci-static/resources/view/fchomo/node.js:1457 +#: htdocs/luci-static/resources/view/fchomo/node.js:1463 +#: htdocs/luci-static/resources/view/fchomo/server.js:231 +#: htdocs/luci-static/resources/view/fchomo/server.js:237 msgid "In Mbps." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1360 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:364 +#: htdocs/luci-static/resources/view/fchomo/node.js:1377 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:362 msgid "In bytes. %s will be used if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:481 -#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:509 msgid "In millisecond." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:990 -#: htdocs/luci-static/resources/view/fchomo/client.js:1019 -#: htdocs/luci-static/resources/view/fchomo/node.js:1511 +#: htdocs/luci-static/resources/view/fchomo/client.js:996 +#: htdocs/luci-static/resources/view/fchomo/client.js:1025 +#: htdocs/luci-static/resources/view/fchomo/node.js:1528 msgid "In millisecond. %s will be used if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:525 -#: htdocs/luci-static/resources/view/fchomo/node.js:532 -#: htdocs/luci-static/resources/view/fchomo/server.js:446 -#: htdocs/luci-static/resources/view/fchomo/server.js:453 +#: htdocs/luci-static/resources/view/fchomo/node.js:546 +#: htdocs/luci-static/resources/view/fchomo/node.js:553 +#: htdocs/luci-static/resources/view/fchomo/server.js:432 +#: htdocs/luci-static/resources/view/fchomo/server.js:439 msgid "In seconds." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:983 +#: htdocs/luci-static/resources/view/fchomo/client.js:989 #: htdocs/luci-static/resources/view/fchomo/global.js:425 #: htdocs/luci-static/resources/view/fchomo/global.js:430 #: htdocs/luci-static/resources/view/fchomo/global.js:515 -#: htdocs/luci-static/resources/view/fchomo/node.js:1366 -#: htdocs/luci-static/resources/view/fchomo/node.js:1504 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:370 +#: htdocs/luci-static/resources/view/fchomo/node.js:1383 +#: htdocs/luci-static/resources/view/fchomo/node.js:1521 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:368 msgid "In seconds. %s will be used if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:779 -#: htdocs/luci-static/resources/view/fchomo/server.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:800 +#: htdocs/luci-static/resources/view/fchomo/server.js:619 msgid "" "In the order of one Padding-Length and one Padding-" "Interval, infinite concatenation." @@ -1333,27 +1349,27 @@ msgstr "" msgid "Inbound" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:951 +#: htdocs/luci-static/resources/view/fchomo/client.js:957 msgid "Include all" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:956 +#: htdocs/luci-static/resources/view/fchomo/client.js:962 msgid "Include all node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:961 +#: htdocs/luci-static/resources/view/fchomo/client.js:967 msgid "Include all provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:962 +#: htdocs/luci-static/resources/view/fchomo/client.js:968 msgid "Includes all Provider." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:952 +#: htdocs/luci-static/resources/view/fchomo/client.js:958 msgid "Includes all Proxy Node and Provider." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:957 +#: htdocs/luci-static/resources/view/fchomo/client.js:963 msgid "Includes all Proxy Node." msgstr "" @@ -1361,8 +1377,8 @@ msgstr "" msgid "Info" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1299 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:266 +#: htdocs/luci-static/resources/view/fchomo/node.js:1316 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 msgid "Inline" msgstr "" @@ -1374,39 +1390,39 @@ msgstr "" msgid "Keep default" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:371 -#: htdocs/luci-static/resources/view/fchomo/server.js:332 +#: htdocs/luci-static/resources/view/fchomo/node.js:373 +#: htdocs/luci-static/resources/view/fchomo/server.js:299 msgid "Key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:904 -#: htdocs/luci-static/resources/view/fchomo/server.js:807 +#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/server.js:793 msgid "Key path" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:652 +#: htdocs/luci-static/resources/view/fchomo/server.js:638 msgid "Keypairs" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:896 -#: htdocs/luci-static/resources/view/fchomo/client.js:1130 -#: htdocs/luci-static/resources/view/fchomo/client.js:1226 -#: htdocs/luci-static/resources/view/fchomo/client.js:1367 -#: htdocs/luci-static/resources/view/fchomo/client.js:1589 -#: htdocs/luci-static/resources/view/fchomo/node.js:227 -#: htdocs/luci-static/resources/view/fchomo/node.js:1287 -#: htdocs/luci-static/resources/view/fchomo/node.js:1572 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 -#: htdocs/luci-static/resources/view/fchomo/server.js:212 +#: htdocs/luci-static/resources/view/fchomo/client.js:902 +#: htdocs/luci-static/resources/view/fchomo/client.js:1132 +#: htdocs/luci-static/resources/view/fchomo/client.js:1224 +#: htdocs/luci-static/resources/view/fchomo/client.js:1361 +#: htdocs/luci-static/resources/view/fchomo/client.js:1579 +#: htdocs/luci-static/resources/view/fchomo/node.js:229 +#: htdocs/luci-static/resources/view/fchomo/node.js:1304 +#: htdocs/luci-static/resources/view/fchomo/node.js:1589 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:244 +#: htdocs/luci-static/resources/view/fchomo/server.js:179 msgid "Label" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:996 -#: htdocs/luci-static/resources/view/fchomo/node.js:1517 +#: htdocs/luci-static/resources/view/fchomo/client.js:1002 +#: htdocs/luci-static/resources/view/fchomo/node.js:1534 msgid "Lazy" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:503 +#: htdocs/luci-static/resources/view/fchomo/server.js:489 msgid "" "Legacy protocol support (VMess MD5 Authentication) is provided for " "compatibility purposes only, use of alterId > 1 is not recommended." @@ -1416,16 +1432,16 @@ msgstr "" msgid "Less compatibility and sometimes better performance." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:831 -#: htdocs/luci-static/resources/view/fchomo/server.js:788 +#: htdocs/luci-static/resources/view/fchomo/node.js:852 +#: htdocs/luci-static/resources/view/fchomo/server.js:774 msgid "List of supported application level protocols, in order of preference." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:232 +#: htdocs/luci-static/resources/view/fchomo/server.js:199 msgid "Listen address" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:209 +#: htdocs/luci-static/resources/view/fchomo/server.js:176 msgid "Listen fields" msgstr "" @@ -1433,8 +1449,8 @@ msgstr "" msgid "Listen interfaces" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1251 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/client.js:1249 +#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Listen port" msgstr "" @@ -1446,16 +1462,16 @@ msgstr "" msgid "Load balance" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1297 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 +#: htdocs/luci-static/resources/view/fchomo/node.js:1314 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:262 msgid "Local" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:602 +#: htdocs/luci-static/resources/view/fchomo/node.js:623 msgid "Local IPv6 address" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:595 +#: htdocs/luci-static/resources/view/fchomo/node.js:616 msgid "Local address" msgstr "" @@ -1480,86 +1496,86 @@ msgid "Lowercase only" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:502 -#: htdocs/luci-static/resources/view/fchomo/node.js:642 +#: htdocs/luci-static/resources/view/fchomo/node.js:663 msgid "MTU" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:295 +#: htdocs/luci-static/resources/view/fchomo/server.js:262 msgid "Masquerade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1611 +#: htdocs/luci-static/resources/view/fchomo/client.js:1601 msgid "Match domain. Support wildcards." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1683 +#: htdocs/luci-static/resources/view/fchomo/client.js:1673 msgid "Match domain. Support wildcards.
" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1616 +#: htdocs/luci-static/resources/view/fchomo/client.js:1606 msgid "Match geosite." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1674 +#: htdocs/luci-static/resources/view/fchomo/client.js:1664 msgid "Match geosite.
" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1655 msgid "Match response with geoip.
" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1678 +#: htdocs/luci-static/resources/view/fchomo/client.js:1668 msgid "Match response with ipcidr.
" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1621 +#: htdocs/luci-static/resources/view/fchomo/client.js:1611 msgid "Match rule set." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1038 +#: htdocs/luci-static/resources/view/fchomo/node.js:1059 msgid "Max Early Data" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:467 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/server.js:425 msgid "Max UDP relay packet size" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1010 +#: htdocs/luci-static/resources/view/fchomo/client.js:1016 msgid "Max count of failures" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:283 -#: htdocs/luci-static/resources/view/fchomo/server.js:269 +#: htdocs/luci-static/resources/view/fchomo/node.js:285 +#: htdocs/luci-static/resources/view/fchomo/server.js:236 msgid "Max download speed" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:494 +#: htdocs/luci-static/resources/view/fchomo/node.js:515 msgid "Max open streams" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:277 -#: htdocs/luci-static/resources/view/fchomo/server.js:263 +#: htdocs/luci-static/resources/view/fchomo/node.js:279 +#: htdocs/luci-static/resources/view/fchomo/server.js:230 msgid "Max upload speed" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1075 -#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/node.js:1096 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "Maximum connections" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1089 +#: htdocs/luci-static/resources/view/fchomo/node.js:1110 msgid "" "Maximum multiplexed streams in a connection before opening a new connection." "
Conflict with %s and %s." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:398 -#: htdocs/luci-static/resources/view/fchomo/server.js:410 +#: htdocs/luci-static/resources/view/fchomo/node.js:412 +#: htdocs/luci-static/resources/view/fchomo/server.js:389 msgid "Maximum padding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1088 +#: htdocs/luci-static/resources/view/fchomo/node.js:1109 msgid "Maximum streams" msgstr "" @@ -1568,7 +1584,7 @@ msgstr "" msgid "Mieru" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:781 +#: htdocs/luci-static/resources/view/fchomo/client.js:791 #: htdocs/luci-static/resources/view/fchomo/log.js:149 #: htdocs/luci-static/resources/view/fchomo/log.js:154 msgid "Mihomo client" @@ -1576,26 +1592,26 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/log.js:158 #: htdocs/luci-static/resources/view/fchomo/log.js:163 -#: htdocs/luci-static/resources/view/fchomo/server.js:164 +#: htdocs/luci-static/resources/view/fchomo/server.js:131 msgid "Mihomo server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:538 +#: htdocs/luci-static/resources/view/fchomo/node.js:559 msgid "Min of idle sessions to keep" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1082 +#: htdocs/luci-static/resources/view/fchomo/node.js:1103 msgid "" "Minimum multiplexed streams in a connection before opening a new connection." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:391 -#: htdocs/luci-static/resources/view/fchomo/server.js:403 +#: htdocs/luci-static/resources/view/fchomo/node.js:405 +#: htdocs/luci-static/resources/view/fchomo/server.js:382 msgid "Minimum padding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1081 -#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/node.js:1102 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "Minimum streams" msgstr "" @@ -1612,33 +1628,33 @@ msgstr "" msgid "Mixed port" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1061 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 msgid "Multiplex" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:224 -#: htdocs/luci-static/resources/view/fchomo/server.js:208 +#: htdocs/luci-static/resources/view/fchomo/node.js:226 +#: htdocs/luci-static/resources/view/fchomo/server.js:175 msgid "Multiplex fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:354 +#: htdocs/luci-static/resources/view/fchomo/node.js:356 msgid "Multiplexing" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:541 -#: htdocs/luci-static/resources/view/fchomo/client.js:616 +#: htdocs/luci-static/resources/view/fchomo/client.js:551 +#: htdocs/luci-static/resources/view/fchomo/client.js:626 msgid "NOT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1372 +#: htdocs/luci-static/resources/view/fchomo/node.js:1389 msgid "Name of the Proxy group to download provider." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:376 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:374 msgid "Name of the Proxy group to download rule set." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:451 +#: htdocs/luci-static/resources/view/fchomo/node.js:472 msgid "Native UDP" msgstr "" @@ -1650,45 +1666,45 @@ msgstr "" msgid "No Authentication IP ranges" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1387 +#: htdocs/luci-static/resources/view/fchomo/client.js:1381 msgid "No add'l params" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:997 -#: htdocs/luci-static/resources/view/fchomo/node.js:1518 +#: htdocs/luci-static/resources/view/fchomo/client.js:1003 +#: htdocs/luci-static/resources/view/fchomo/node.js:1535 msgid "No testing is performed when this provider node is not in use." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:570 +#: htdocs/luci-static/resources/fchomo.js:640 msgid "No valid %s found." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:207 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:205 msgid "No valid rule-set link found." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:923 -#: htdocs/luci-static/resources/view/fchomo/node.js:215 +#: htdocs/luci-static/resources/view/fchomo/client.js:929 +#: htdocs/luci-static/resources/view/fchomo/node.js:217 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 msgid "Node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1042 -#: htdocs/luci-static/resources/view/fchomo/node.js:1538 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +#: htdocs/luci-static/resources/view/fchomo/node.js:1555 msgid "Node exclude filter" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1047 -#: htdocs/luci-static/resources/view/fchomo/node.js:1545 +#: htdocs/luci-static/resources/view/fchomo/client.js:1053 +#: htdocs/luci-static/resources/view/fchomo/node.js:1562 msgid "Node exclude type" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1037 -#: htdocs/luci-static/resources/view/fchomo/node.js:1532 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +#: htdocs/luci-static/resources/view/fchomo/node.js:1549 msgid "Node filter" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1018 +#: htdocs/luci-static/resources/view/fchomo/client.js:1024 msgid "Node switch tolerance" msgstr "" @@ -1700,37 +1716,37 @@ msgstr "" msgid "Not Installed" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1038 +#: htdocs/luci-static/resources/fchomo.js:1088 msgid "Not Running" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:671 +#: htdocs/luci-static/resources/view/fchomo/node.js:692 msgid "Obfs Mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:295 -#: htdocs/luci-static/resources/view/fchomo/server.js:287 +#: htdocs/luci-static/resources/view/fchomo/node.js:297 +#: htdocs/luci-static/resources/view/fchomo/server.js:254 msgid "Obfuscate password" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:289 -#: htdocs/luci-static/resources/view/fchomo/node.js:385 -#: htdocs/luci-static/resources/view/fchomo/server.js:281 -#: htdocs/luci-static/resources/view/fchomo/server.js:397 +#: htdocs/luci-static/resources/view/fchomo/node.js:291 +#: htdocs/luci-static/resources/view/fchomo/node.js:395 +#: htdocs/luci-static/resources/view/fchomo/server.js:248 +#: htdocs/luci-static/resources/view/fchomo/server.js:372 msgid "Obfuscate type" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:386 -#: htdocs/luci-static/resources/view/fchomo/server.js:398 +#: htdocs/luci-static/resources/view/fchomo/node.js:396 +#: htdocs/luci-static/resources/view/fchomo/server.js:373 msgid "Obfuscated as ASCII data stream" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:387 -#: htdocs/luci-static/resources/view/fchomo/server.js:399 +#: htdocs/luci-static/resources/view/fchomo/node.js:397 +#: htdocs/luci-static/resources/view/fchomo/server.js:374 msgid "Obfuscated as low-entropy data stream" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:871 +#: htdocs/luci-static/resources/view/fchomo/global.js:886 msgid "One or more numbers in the range 0-63 separated by commas" msgstr "" @@ -1738,7 +1754,7 @@ msgstr "" msgid "Only process traffic from specific interfaces. Leave empty for all." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1032 +#: htdocs/luci-static/resources/fchomo.js:1082 msgid "Open Dashboard" msgstr "" @@ -1751,16 +1767,16 @@ msgstr "" msgid "Override destination" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:892 -#: htdocs/luci-static/resources/view/fchomo/node.js:1283 +#: htdocs/luci-static/resources/view/fchomo/client.js:898 +#: htdocs/luci-static/resources/view/fchomo/node.js:1300 msgid "Override fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:434 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 msgid "Override the IP address of the server that DNS response." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1644 +#: htdocs/luci-static/resources/view/fchomo/client.js:1634 msgid "Override the Proxy group of DNS server." msgstr "" @@ -1768,7 +1784,7 @@ msgstr "" msgid "Override the connection destination address with the sniffed domain." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1491 +#: htdocs/luci-static/resources/view/fchomo/client.js:1485 msgid "Override the existing ECS in original request." msgstr "" @@ -1776,43 +1792,43 @@ msgstr "" msgid "Overview" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:1028 msgid "POST" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1029 msgid "PUT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:587 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 msgid "Packet encoding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:483 +#: htdocs/luci-static/resources/view/fchomo/server.js:469 msgid "Padding scheme" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:777 -#: htdocs/luci-static/resources/view/fchomo/server.js:631 +#: htdocs/luci-static/resources/view/fchomo/node.js:798 +#: htdocs/luci-static/resources/view/fchomo/server.js:617 msgid "Paddings" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:259 -#: htdocs/luci-static/resources/view/fchomo/node.js:332 -#: htdocs/luci-static/resources/view/fchomo/node.js:686 -#: htdocs/luci-static/resources/view/fchomo/server.js:254 -#: htdocs/luci-static/resources/view/fchomo/server.js:310 -#: htdocs/luci-static/resources/view/fchomo/server.js:524 +#: htdocs/luci-static/resources/view/fchomo/node.js:261 +#: htdocs/luci-static/resources/view/fchomo/node.js:334 +#: htdocs/luci-static/resources/view/fchomo/node.js:707 +#: htdocs/luci-static/resources/view/fchomo/server.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:277 +#: htdocs/luci-static/resources/view/fchomo/server.js:510 msgid "Password" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1347 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:351 -#: htdocs/luci-static/resources/view/fchomo/server.js:571 +#: htdocs/luci-static/resources/view/fchomo/node.js:1364 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:349 +#: htdocs/luci-static/resources/view/fchomo/server.js:557 msgid "Payload" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:616 +#: htdocs/luci-static/resources/view/fchomo/node.js:637 msgid "Peer pubkic key" msgstr "" @@ -1822,68 +1838,68 @@ msgid "" "it is not needed." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:282 msgid "Plain text" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:848 +#: htdocs/luci-static/resources/view/fchomo/global.js:863 msgid "" "Please ensure that the DNS query of the domains to be processed in the DNS " "policy
are send via DIRECT/Proxy Node in the same semantics as Routing " "mode." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:184 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:182 msgid "" "Please refer to %s for link format " "standards." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1320 -#: htdocs/luci-static/resources/view/fchomo/node.js:1346 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:324 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:350 +#: htdocs/luci-static/resources/view/fchomo/node.js:1337 +#: htdocs/luci-static/resources/view/fchomo/node.js:1363 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:322 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:348 msgid "" "Please type %s." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:827 -#: htdocs/luci-static/resources/view/fchomo/client.js:1083 -#: htdocs/luci-static/resources/view/fchomo/client.js:1193 -#: htdocs/luci-static/resources/view/fchomo/client.js:1322 -#: htdocs/luci-static/resources/view/fchomo/client.js:1559 -#: htdocs/luci-static/resources/view/fchomo/node.js:1185 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:141 +#: htdocs/luci-static/resources/view/fchomo/client.js:837 +#: htdocs/luci-static/resources/view/fchomo/client.js:1089 +#: htdocs/luci-static/resources/view/fchomo/client.js:1195 +#: htdocs/luci-static/resources/view/fchomo/client.js:1320 +#: htdocs/luci-static/resources/view/fchomo/client.js:1553 +#: htdocs/luci-static/resources/view/fchomo/node.js:1206 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:143 msgid "Please type %s fields of mihomo config.
" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:660 -#: htdocs/luci-static/resources/view/fchomo/server.js:510 +#: htdocs/luci-static/resources/view/fchomo/node.js:681 +#: htdocs/luci-static/resources/view/fchomo/server.js:496 msgid "Plugin" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:671 -#: htdocs/luci-static/resources/view/fchomo/node.js:678 -#: htdocs/luci-static/resources/view/fchomo/node.js:686 #: htdocs/luci-static/resources/view/fchomo/node.js:692 -#: htdocs/luci-static/resources/view/fchomo/node.js:700 -#: htdocs/luci-static/resources/view/fchomo/node.js:706 -#: htdocs/luci-static/resources/view/fchomo/server.js:517 -#: htdocs/luci-static/resources/view/fchomo/server.js:524 -#: htdocs/luci-static/resources/view/fchomo/server.js:530 +#: htdocs/luci-static/resources/view/fchomo/node.js:699 +#: htdocs/luci-static/resources/view/fchomo/node.js:707 +#: htdocs/luci-static/resources/view/fchomo/node.js:713 +#: htdocs/luci-static/resources/view/fchomo/node.js:721 +#: htdocs/luci-static/resources/view/fchomo/node.js:727 +#: htdocs/luci-static/resources/view/fchomo/server.js:503 +#: htdocs/luci-static/resources/view/fchomo/server.js:510 +#: htdocs/luci-static/resources/view/fchomo/server.js:516 msgid "Plugin:" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:247 +#: htdocs/luci-static/resources/view/fchomo/node.js:249 msgid "Port" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1263 +#: htdocs/luci-static/resources/fchomo.js:1313 msgid "Port %s alrealy exists!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:342 +#: htdocs/luci-static/resources/view/fchomo/node.js:344 msgid "Port range" msgstr "" @@ -1891,13 +1907,13 @@ msgstr "" msgid "Ports" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:272 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/node.js:274 +#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Ports pool" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:411 -#: htdocs/luci-static/resources/view/fchomo/node.js:623 +#: htdocs/luci-static/resources/view/fchomo/node.js:432 +#: htdocs/luci-static/resources/view/fchomo/node.js:644 msgid "Pre-shared key" msgstr "" @@ -1916,22 +1932,22 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:742 #: htdocs/luci-static/resources/view/fchomo/global.js:759 -#: htdocs/luci-static/resources/view/fchomo/node.js:1149 -#: htdocs/luci-static/resources/view/fchomo/node.js:1155 -#: htdocs/luci-static/resources/view/fchomo/node.js:1467 -#: htdocs/luci-static/resources/view/fchomo/node.js:1474 +#: htdocs/luci-static/resources/view/fchomo/node.js:1170 +#: htdocs/luci-static/resources/view/fchomo/node.js:1176 +#: htdocs/luci-static/resources/view/fchomo/node.js:1484 +#: htdocs/luci-static/resources/view/fchomo/node.js:1491 msgid "Priority: Proxy Node > Global." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:304 +#: htdocs/luci-static/resources/view/fchomo/node.js:306 msgid "Priv-key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:308 +#: htdocs/luci-static/resources/view/fchomo/node.js:310 msgid "Priv-key passphrase" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:608 +#: htdocs/luci-static/resources/view/fchomo/node.js:629 msgid "Private key" msgstr "" @@ -1940,34 +1956,34 @@ msgid "Process matching mode" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:690 -#: htdocs/luci-static/resources/view/fchomo/node.js:1067 +#: htdocs/luci-static/resources/view/fchomo/node.js:1088 msgid "Protocol" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:582 +#: htdocs/luci-static/resources/view/fchomo/node.js:603 msgid "Protocol parameter. Enable length block encryption." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:576 +#: htdocs/luci-static/resources/view/fchomo/node.js:597 msgid "" "Protocol parameter. Will waste traffic randomly if enabled (enabled by " "default in v2ray and cannot be disabled)." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:937 -#: htdocs/luci-static/resources/view/fchomo/node.js:1168 -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 -#: htdocs/luci-static/resources/view/fchomo/node.js:1583 +#: htdocs/luci-static/resources/view/fchomo/client.js:943 +#: htdocs/luci-static/resources/view/fchomo/node.js:1189 +#: htdocs/luci-static/resources/view/fchomo/node.js:1198 +#: htdocs/luci-static/resources/view/fchomo/node.js:1600 msgid "Provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1353 +#: htdocs/luci-static/resources/view/fchomo/node.js:1370 msgid "Provider URL" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:801 -#: htdocs/luci-static/resources/view/fchomo/client.js:819 -#: htdocs/luci-static/resources/view/fchomo/client.js:1278 +#: htdocs/luci-static/resources/view/fchomo/client.js:811 +#: htdocs/luci-static/resources/view/fchomo/client.js:829 +#: htdocs/luci-static/resources/view/fchomo/client.js:1276 msgid "Proxy Group" msgstr "" @@ -1983,24 +1999,24 @@ msgstr "" msgid "Proxy MAC-s" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:206 -#: htdocs/luci-static/resources/view/fchomo/node.js:1582 +#: htdocs/luci-static/resources/view/fchomo/node.js:208 +#: htdocs/luci-static/resources/view/fchomo/node.js:1599 msgid "Proxy Node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1558 -#: htdocs/luci-static/resources/view/fchomo/node.js:1567 +#: htdocs/luci-static/resources/view/fchomo/node.js:1575 +#: htdocs/luci-static/resources/view/fchomo/node.js:1584 msgid "Proxy chain" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:713 -#: htdocs/luci-static/resources/view/fchomo/client.js:1422 -#: htdocs/luci-static/resources/view/fchomo/node.js:1371 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:375 +#: htdocs/luci-static/resources/view/fchomo/client.js:723 +#: htdocs/luci-static/resources/view/fchomo/client.js:1416 +#: htdocs/luci-static/resources/view/fchomo/node.js:1388 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:373 msgid "Proxy group" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1643 +#: htdocs/luci-static/resources/view/fchomo/client.js:1633 msgid "Proxy group override" msgstr "" @@ -2012,54 +2028,54 @@ msgstr "" msgid "Proxy routerself" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:452 +#: htdocs/luci-static/resources/view/fchomo/node.js:473 msgid "QUIC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:440 -#: htdocs/luci-static/resources/view/fchomo/server.js:431 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/server.js:417 msgid "QUIC congestion controller." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:804 -#: htdocs/luci-static/resources/view/fchomo/server.js:185 +#: htdocs/luci-static/resources/view/fchomo/client.js:814 +#: htdocs/luci-static/resources/view/fchomo/server.js:152 msgid "Quick Reload" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:940 -#: htdocs/luci-static/resources/view/fchomo/server.js:895 +#: htdocs/luci-static/resources/view/fchomo/node.js:961 +#: htdocs/luci-static/resources/view/fchomo/server.js:881 msgid "REALITY" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:955 +#: htdocs/luci-static/resources/view/fchomo/node.js:976 msgid "REALITY X25519MLKEM768 PQC support" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:918 msgid "REALITY certificate issued to" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:900 +#: htdocs/luci-static/resources/view/fchomo/server.js:886 msgid "REALITY handshake server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:907 +#: htdocs/luci-static/resources/view/fchomo/server.js:893 msgid "REALITY private key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:945 -#: htdocs/luci-static/resources/view/fchomo/server.js:922 +#: htdocs/luci-static/resources/view/fchomo/node.js:966 +#: htdocs/luci-static/resources/view/fchomo/server.js:908 msgid "REALITY public key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:950 -#: htdocs/luci-static/resources/view/fchomo/server.js:926 +#: htdocs/luci-static/resources/view/fchomo/node.js:971 +#: htdocs/luci-static/resources/view/fchomo/server.js:912 msgid "REALITY short ID" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:767 -#: htdocs/luci-static/resources/view/fchomo/server.js:602 -#: htdocs/luci-static/resources/view/fchomo/server.js:621 +#: htdocs/luci-static/resources/view/fchomo/node.js:788 +#: htdocs/luci-static/resources/view/fchomo/server.js:588 +#: htdocs/luci-static/resources/view/fchomo/server.js:607 msgid "RTT" msgstr "" @@ -2095,10 +2111,10 @@ msgstr "" msgid "Refresh every %s seconds." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1025 -#: htdocs/luci-static/resources/view/fchomo/client.js:805 +#: htdocs/luci-static/resources/fchomo.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:815 #: htdocs/luci-static/resources/view/fchomo/global.js:193 -#: htdocs/luci-static/resources/view/fchomo/server.js:186 +#: htdocs/luci-static/resources/view/fchomo/server.js:153 msgid "Reload" msgstr "" @@ -2106,32 +2122,32 @@ msgstr "" msgid "Reload All" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1298 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:265 +#: htdocs/luci-static/resources/view/fchomo/node.js:1315 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 msgid "Remote" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:648 +#: htdocs/luci-static/resources/view/fchomo/node.js:669 msgid "Remote DNS resolve" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1184 +#: htdocs/luci-static/resources/fchomo.js:1234 msgid "Remove" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1189 -#: htdocs/luci-static/resources/view/fchomo/node.js:1274 -#: htdocs/luci-static/resources/view/fchomo/node.js:1276 +#: htdocs/luci-static/resources/fchomo.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1291 +#: htdocs/luci-static/resources/view/fchomo/node.js:1293 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:236 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:238 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:240 msgid "Remove idles" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1400 +#: htdocs/luci-static/resources/view/fchomo/node.js:1417 msgid "Replace name" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1401 +#: htdocs/luci-static/resources/view/fchomo/node.js:1418 msgid "Replace node name." msgstr "" @@ -2139,13 +2155,13 @@ msgstr "" msgid "Request" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1014 -#: htdocs/luci-static/resources/view/fchomo/node.js:1021 -#: htdocs/luci-static/resources/view/fchomo/server.js:974 +#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/view/fchomo/node.js:1042 +#: htdocs/luci-static/resources/view/fchomo/server.js:960 msgid "Request path" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:487 +#: htdocs/luci-static/resources/view/fchomo/node.js:508 msgid "Request timeout" msgstr "" @@ -2158,11 +2174,11 @@ msgid "Require any" msgstr "" #: htdocs/luci-static/resources/fchomo.js:347 -#: htdocs/luci-static/resources/view/fchomo/node.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:977 msgid "Requires server support." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:637 +#: htdocs/luci-static/resources/view/fchomo/node.js:658 msgid "Reserved field bytes" msgstr "" @@ -2170,16 +2186,16 @@ msgstr "" msgid "Resources management" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:706 +#: htdocs/luci-static/resources/view/fchomo/node.js:727 msgid "Restls script" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1054 +#: htdocs/luci-static/resources/view/fchomo/client.js:1060 msgid "" "Returns hidden status in the API to hide the display of this proxy group." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1060 +#: htdocs/luci-static/resources/view/fchomo/client.js:1066 msgid "" "Returns the string input for icon in the API to display in this proxy group." msgstr "" @@ -2188,17 +2204,17 @@ msgstr "" msgid "Routing Control" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:860 -#: htdocs/luci-static/resources/view/fchomo/global.js:863 +#: htdocs/luci-static/resources/view/fchomo/global.js:875 +#: htdocs/luci-static/resources/view/fchomo/global.js:878 msgid "Routing DSCP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:844 +#: htdocs/luci-static/resources/view/fchomo/global.js:845 msgid "Routing GFW" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1154 -#: htdocs/luci-static/resources/view/fchomo/node.js:1473 +#: htdocs/luci-static/resources/view/fchomo/node.js:1175 +#: htdocs/luci-static/resources/view/fchomo/node.js:1490 msgid "Routing mark" msgstr "" @@ -2214,7 +2230,7 @@ msgstr "" msgid "Routing mode of the traffic enters mihomo via firewall rules." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:847 +#: htdocs/luci-static/resources/view/fchomo/global.js:862 msgid "Routing mode will be handle domain." msgstr "" @@ -2223,8 +2239,8 @@ msgstr "" msgid "Routing ports" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1066 -#: htdocs/luci-static/resources/view/fchomo/client.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:1072 +#: htdocs/luci-static/resources/view/fchomo/client.js:1081 msgid "Routing rule" msgstr "" @@ -2240,13 +2256,13 @@ msgstr "" msgid "Rule" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1607 -#: htdocs/luci-static/resources/view/fchomo/client.js:1620 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:133 +#: htdocs/luci-static/resources/view/fchomo/client.js:1597 +#: htdocs/luci-static/resources/view/fchomo/client.js:1610 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:135 msgid "Rule set" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:357 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:355 msgid "Rule set URL" msgstr "" @@ -2254,11 +2270,11 @@ msgstr "" msgid "Ruleset" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:185 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:183 msgid "Ruleset-URI-Scheme" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1038 +#: htdocs/luci-static/resources/fchomo.js:1088 msgid "Running" msgstr "" @@ -2282,16 +2298,16 @@ msgstr "" msgid "STUN ports" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1149 +#: htdocs/luci-static/resources/view/fchomo/client.js:1151 msgid "SUB-RULE" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:724 +#: htdocs/luci-static/resources/view/fchomo/node.js:745 msgid "SUoT version" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:291 -#: htdocs/luci-static/resources/view/fchomo/server.js:283 +#: htdocs/luci-static/resources/view/fchomo/node.js:293 +#: htdocs/luci-static/resources/view/fchomo/server.js:250 msgid "Salamander" msgstr "" @@ -2324,19 +2340,19 @@ msgstr "" msgid "Send random ticket of 300s-600s duration for client 0-RTT reuse." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 -#: htdocs/luci-static/resources/view/fchomo/server.js:602 -#: htdocs/luci-static/resources/view/fchomo/server.js:793 -#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/server.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:588 +#: htdocs/luci-static/resources/view/fchomo/server.js:779 +#: htdocs/luci-static/resources/view/fchomo/server.js:794 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 msgid "Server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:242 +#: htdocs/luci-static/resources/view/fchomo/node.js:244 msgid "Server address" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:1020 msgid "Server hostname" msgstr "" @@ -2353,22 +2369,22 @@ msgstr "" msgid "Shadowsocks" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:506 -#: htdocs/luci-static/resources/view/fchomo/server.js:465 +#: htdocs/luci-static/resources/view/fchomo/node.js:527 +#: htdocs/luci-static/resources/view/fchomo/server.js:451 msgid "Shadowsocks chipher" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:501 -#: htdocs/luci-static/resources/view/fchomo/server.js:460 +#: htdocs/luci-static/resources/view/fchomo/node.js:522 +#: htdocs/luci-static/resources/view/fchomo/server.js:446 msgid "Shadowsocks encrypt" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:514 -#: htdocs/luci-static/resources/view/fchomo/server.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:535 +#: htdocs/luci-static/resources/view/fchomo/server.js:459 msgid "Shadowsocks password" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1109 +#: htdocs/luci-static/resources/view/fchomo/node.js:1130 msgid "Show connections in the dashboard for breaking connections easier." msgstr "" @@ -2380,14 +2396,14 @@ msgstr "" msgid "Simple round-robin all nodes" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1359 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:363 +#: htdocs/luci-static/resources/view/fchomo/node.js:1376 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:361 msgid "Size limit" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1455 -#: htdocs/luci-static/resources/view/fchomo/node.js:882 -#: htdocs/luci-static/resources/view/fchomo/node.js:1451 +#: htdocs/luci-static/resources/view/fchomo/client.js:1449 +#: htdocs/luci-static/resources/view/fchomo/node.js:903 +#: htdocs/luci-static/resources/view/fchomo/node.js:1468 msgid "Skip cert verify" msgstr "" @@ -2438,22 +2454,22 @@ msgstr "" msgid "Steam P2P ports" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1026 -#: htdocs/luci-static/resources/view/fchomo/client.js:1028 +#: htdocs/luci-static/resources/view/fchomo/client.js:1032 +#: htdocs/luci-static/resources/view/fchomo/client.js:1034 msgid "Strategy" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1176 -#: htdocs/luci-static/resources/view/fchomo/client.js:1185 +#: htdocs/luci-static/resources/view/fchomo/client.js:1178 +#: htdocs/luci-static/resources/view/fchomo/client.js:1187 msgid "Sub rule" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1235 +#: htdocs/luci-static/resources/view/fchomo/client.js:1233 msgid "Sub rule group" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:573 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:209 +#: htdocs/luci-static/resources/fchomo.js:643 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:207 msgid "Successfully imported %s %s of total %s." msgstr "" @@ -2461,7 +2477,7 @@ msgstr "" msgid "Successfully updated." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1518 +#: htdocs/luci-static/resources/fchomo.js:1568 msgid "Successfully uploaded." msgstr "" @@ -2470,7 +2486,7 @@ msgstr "" msgid "Sudoku" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:182 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:180 msgid "" "Supports rule-set links of type: %s and format: %s." "
" @@ -2480,7 +2496,7 @@ msgstr "" msgid "System" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:54 +#: htdocs/luci-static/resources/view/fchomo/client.js:56 msgid "System DNS" msgstr "" @@ -2498,8 +2514,8 @@ msgstr "" #: htdocs/luci-static/resources/fchomo.js:167 #: htdocs/luci-static/resources/fchomo.js:168 #: htdocs/luci-static/resources/fchomo.js:173 -#: htdocs/luci-static/resources/view/fchomo/client.js:517 -#: htdocs/luci-static/resources/view/fchomo/client.js:607 +#: htdocs/luci-static/resources/view/fchomo/client.js:527 +#: htdocs/luci-static/resources/view/fchomo/client.js:617 msgid "TCP" msgstr "" @@ -2507,7 +2523,7 @@ msgstr "" msgid "TCP concurrency" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1102 +#: htdocs/luci-static/resources/view/fchomo/node.js:1123 msgid "TCP only" msgstr "" @@ -2530,29 +2546,29 @@ msgstr "" msgid "TCP/UDP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1133 -#: htdocs/luci-static/resources/view/fchomo/node.js:1418 +#: htdocs/luci-static/resources/view/fchomo/node.js:1154 +#: htdocs/luci-static/resources/view/fchomo/node.js:1435 msgid "TFO" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:529 -#: htdocs/luci-static/resources/view/fchomo/node.js:673 -#: htdocs/luci-static/resources/view/fchomo/node.js:799 -#: htdocs/luci-static/resources/view/fchomo/server.js:756 +#: htdocs/luci-static/resources/view/fchomo/node.js:694 +#: htdocs/luci-static/resources/view/fchomo/node.js:820 +#: htdocs/luci-static/resources/view/fchomo/server.js:742 msgid "TLS" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:851 +#: htdocs/luci-static/resources/view/fchomo/server.js:773 msgid "TLS ALPN" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:824 +#: htdocs/luci-static/resources/view/fchomo/node.js:845 msgid "TLS SNI" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:222 -#: htdocs/luci-static/resources/view/fchomo/server.js:206 +#: htdocs/luci-static/resources/view/fchomo/node.js:224 +#: htdocs/luci-static/resources/view/fchomo/server.js:173 msgid "TLS fields" msgstr "" @@ -2565,39 +2581,39 @@ msgstr "" msgid "TURN ports" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:276 +#: htdocs/luci-static/resources/view/fchomo/server.js:243 msgid "" "Tell the client to use the BBR flow control algorithm instead of Hysteria CC." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:596 -#: htdocs/luci-static/resources/view/fchomo/node.js:603 +#: htdocs/luci-static/resources/view/fchomo/node.js:617 +#: htdocs/luci-static/resources/view/fchomo/node.js:624 msgid "The %s address used by local machine in the Wireguard network." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 +#: htdocs/luci-static/resources/view/fchomo/server.js:794 msgid "The %s private key, in PEM format." msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:556 -#: htdocs/luci-static/resources/view/fchomo/node.js:891 -#: htdocs/luci-static/resources/view/fchomo/server.js:793 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/view/fchomo/node.js:912 +#: htdocs/luci-static/resources/view/fchomo/server.js:779 +#: htdocs/luci-static/resources/view/fchomo/server.js:817 msgid "The %s public key, in PEM format." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/node.js:946 msgid "" "The ECH parameter of the HTTPS record for the domain. Leave empty to resolve " "via DNS." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:372 +#: htdocs/luci-static/resources/view/fchomo/node.js:374 msgid "The ED25519 available private key or UUID provided by Sudoku server." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:333 +#: htdocs/luci-static/resources/view/fchomo/server.js:300 msgid "The ED25519 master public key or UUID generated by Sudoku." msgstr "" @@ -2605,41 +2621,41 @@ msgstr "" msgid "The default value is 2:00 every day." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:780 -#: htdocs/luci-static/resources/view/fchomo/server.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:801 +#: htdocs/luci-static/resources/view/fchomo/server.js:620 msgid "" "The first padding must have a probability of 100% and at least 35 bytes." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1666 +#: htdocs/luci-static/resources/view/fchomo/client.js:1656 msgid "The matching %s will be deemed as not-poisoned." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1675 -#: htdocs/luci-static/resources/view/fchomo/client.js:1679 -#: htdocs/luci-static/resources/view/fchomo/client.js:1684 +#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1669 +#: htdocs/luci-static/resources/view/fchomo/client.js:1674 msgid "The matching %s will be deemed as poisoned." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:778 -#: htdocs/luci-static/resources/view/fchomo/server.js:632 +#: htdocs/luci-static/resources/view/fchomo/node.js:799 +#: htdocs/luci-static/resources/view/fchomo/server.js:618 msgid "The server and client can set different padding parameters." msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:600 -#: htdocs/luci-static/resources/view/fchomo/server.js:889 +#: htdocs/luci-static/resources/view/fchomo/server.js:875 msgid "This ECH parameter needs to be added to the HTTPS record of the domain." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1458 -#: htdocs/luci-static/resources/view/fchomo/node.js:885 -#: htdocs/luci-static/resources/view/fchomo/node.js:1454 +#: htdocs/luci-static/resources/view/fchomo/client.js:1452 +#: htdocs/luci-static/resources/view/fchomo/node.js:906 +#: htdocs/luci-static/resources/view/fchomo/node.js:1471 msgid "" "This is DANGEROUS, your traffic is almost like " "PLAIN TEXT! Use at your own risk!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:457 +#: htdocs/luci-static/resources/view/fchomo/node.js:478 msgid "" "This is the TUIC port of the SUoT protocol, designed to provide a QUIC " "stream based UDP relay mode that TUIC does not provide." @@ -2656,7 +2672,7 @@ msgid "" "kmod-tun" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:852 +#: htdocs/luci-static/resources/view/fchomo/global.js:867 msgid "To enable, you need to install dnsmasq-full." msgstr "" @@ -2668,32 +2684,32 @@ msgstr "" msgid "Tproxy port" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1610 +#: htdocs/luci-static/resources/view/fchomo/node.js:1627 msgid "Transit proxy group" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1616 +#: htdocs/luci-static/resources/view/fchomo/node.js:1633 msgid "Transit proxy node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:347 -#: htdocs/luci-static/resources/view/fchomo/node.js:962 -#: htdocs/luci-static/resources/view/fchomo/server.js:320 -#: htdocs/luci-static/resources/view/fchomo/server.js:940 +#: htdocs/luci-static/resources/view/fchomo/node.js:349 +#: htdocs/luci-static/resources/view/fchomo/node.js:983 +#: htdocs/luci-static/resources/view/fchomo/server.js:287 +#: htdocs/luci-static/resources/view/fchomo/server.js:926 msgid "Transport" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:223 -#: htdocs/luci-static/resources/view/fchomo/server.js:207 +#: htdocs/luci-static/resources/view/fchomo/node.js:225 +#: htdocs/luci-static/resources/view/fchomo/server.js:174 msgid "Transport fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:967 -#: htdocs/luci-static/resources/view/fchomo/server.js:945 +#: htdocs/luci-static/resources/view/fchomo/node.js:988 +#: htdocs/luci-static/resources/view/fchomo/server.js:931 msgid "Transport type" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:732 +#: htdocs/luci-static/resources/view/fchomo/client.js:742 msgid "Treat the destination IP as the source IP." msgstr "" @@ -2718,16 +2734,16 @@ msgstr "" msgid "Tun stack." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:458 -#: htdocs/luci-static/resources/view/fchomo/client.js:571 -#: htdocs/luci-static/resources/view/fchomo/client.js:665 -#: htdocs/luci-static/resources/view/fchomo/client.js:910 -#: htdocs/luci-static/resources/view/fchomo/client.js:1604 -#: htdocs/luci-static/resources/view/fchomo/node.js:236 -#: htdocs/luci-static/resources/view/fchomo/node.js:1296 -#: htdocs/luci-static/resources/view/fchomo/node.js:1581 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 -#: htdocs/luci-static/resources/view/fchomo/server.js:226 +#: htdocs/luci-static/resources/view/fchomo/client.js:468 +#: htdocs/luci-static/resources/view/fchomo/client.js:581 +#: htdocs/luci-static/resources/view/fchomo/client.js:675 +#: htdocs/luci-static/resources/view/fchomo/client.js:916 +#: htdocs/luci-static/resources/view/fchomo/client.js:1594 +#: htdocs/luci-static/resources/view/fchomo/node.js:238 +#: htdocs/luci-static/resources/view/fchomo/node.js:1313 +#: htdocs/luci-static/resources/view/fchomo/node.js:1598 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:261 +#: htdocs/luci-static/resources/view/fchomo/server.js:193 msgid "Type" msgstr "" @@ -2736,11 +2752,11 @@ msgstr "" #: htdocs/luci-static/resources/fchomo.js:170 #: htdocs/luci-static/resources/fchomo.js:171 #: htdocs/luci-static/resources/fchomo.js:172 -#: htdocs/luci-static/resources/view/fchomo/client.js:516 -#: htdocs/luci-static/resources/view/fchomo/client.js:606 -#: htdocs/luci-static/resources/view/fchomo/node.js:713 -#: htdocs/luci-static/resources/view/fchomo/node.js:1428 -#: htdocs/luci-static/resources/view/fchomo/server.js:539 +#: htdocs/luci-static/resources/view/fchomo/client.js:526 +#: htdocs/luci-static/resources/view/fchomo/client.js:616 +#: htdocs/luci-static/resources/view/fchomo/node.js:734 +#: htdocs/luci-static/resources/view/fchomo/node.js:1445 +#: htdocs/luci-static/resources/view/fchomo/server.js:525 msgid "UDP" msgstr "" @@ -2748,19 +2764,19 @@ msgstr "" msgid "UDP NAT expiration time" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 msgid "UDP over stream" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:462 +#: htdocs/luci-static/resources/view/fchomo/node.js:483 msgid "UDP over stream version" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:449 +#: htdocs/luci-static/resources/view/fchomo/node.js:470 msgid "UDP packet relay mode." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 +#: htdocs/luci-static/resources/view/fchomo/node.js:469 msgid "UDP relay mode" msgstr "" @@ -2768,15 +2784,15 @@ msgstr "" msgid "URL test" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/node.js:545 -#: htdocs/luci-static/resources/view/fchomo/server.js:330 -#: htdocs/luci-static/resources/view/fchomo/server.js:424 -#: htdocs/luci-static/resources/view/fchomo/server.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:448 +#: htdocs/luci-static/resources/view/fchomo/node.js:566 +#: htdocs/luci-static/resources/view/fchomo/server.js:297 +#: htdocs/luci-static/resources/view/fchomo/server.js:410 +#: htdocs/luci-static/resources/view/fchomo/server.js:474 msgid "UUID" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1083 +#: htdocs/luci-static/resources/fchomo.js:1133 msgid "Unable to download unsupported type: %s" msgstr "" @@ -2801,8 +2817,8 @@ msgstr "" msgid "Unknown error: %s" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:718 -#: htdocs/luci-static/resources/view/fchomo/node.js:1433 +#: htdocs/luci-static/resources/view/fchomo/node.js:739 +#: htdocs/luci-static/resources/view/fchomo/node.js:1450 msgid "UoT" msgstr "" @@ -2810,22 +2826,29 @@ msgstr "" msgid "Update failed." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1365 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:369 +#: htdocs/luci-static/resources/view/fchomo/node.js:1382 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:367 msgid "Update interval" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1120 +#: htdocs/luci-static/resources/view/fchomo/node.js:426 +#: htdocs/luci-static/resources/view/fchomo/server.js:404 +msgid "" +"Uplink keeps the Sudoku protocol, and downlink characteristics are " +"consistent with uplink characteristics." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1141 msgid "Upload bandwidth" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1121 +#: htdocs/luci-static/resources/view/fchomo/node.js:1142 msgid "Upload bandwidth in Mbps." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:896 -#: htdocs/luci-static/resources/view/fchomo/server.js:799 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 +#: htdocs/luci-static/resources/view/fchomo/node.js:917 +#: htdocs/luci-static/resources/view/fchomo/server.js:785 +#: htdocs/luci-static/resources/view/fchomo/server.js:825 msgid "Upload certificate" msgstr "" @@ -2833,41 +2856,41 @@ msgstr "" msgid "Upload initial package" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:910 -#: htdocs/luci-static/resources/view/fchomo/server.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/server.js:800 msgid "Upload key" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:306 -#: htdocs/luci-static/resources/view/fchomo/node.js:899 -#: htdocs/luci-static/resources/view/fchomo/node.js:913 -#: htdocs/luci-static/resources/view/fchomo/server.js:802 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:842 +#: htdocs/luci-static/resources/view/fchomo/node.js:920 +#: htdocs/luci-static/resources/view/fchomo/node.js:934 +#: htdocs/luci-static/resources/view/fchomo/server.js:788 +#: htdocs/luci-static/resources/view/fchomo/server.js:803 +#: htdocs/luci-static/resources/view/fchomo/server.js:828 msgid "Upload..." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1276 +#: htdocs/luci-static/resources/view/fchomo/client.js:1274 msgid "" "Used to resolve domains that can be directly connected. Can use domestic DNS " "servers or ECS." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1278 +#: htdocs/luci-static/resources/view/fchomo/client.js:1276 msgid "" "Used to resolve domains you want to proxy. Recommended to configure %s for " "DNS servers." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1260 +#: htdocs/luci-static/resources/view/fchomo/client.js:1258 msgid "Used to resolve the domain of the DNS server. Must be IP." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1267 +#: htdocs/luci-static/resources/view/fchomo/client.js:1265 msgid "Used to resolve the domain of the Proxy node." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:825 +#: htdocs/luci-static/resources/view/fchomo/node.js:846 msgid "Used to verify the hostname on the returned certificates." msgstr "" @@ -2875,8 +2898,8 @@ msgstr "" msgid "User Authentication" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:254 -#: htdocs/luci-static/resources/view/fchomo/server.js:249 +#: htdocs/luci-static/resources/view/fchomo/node.js:256 +#: htdocs/luci-static/resources/view/fchomo/server.js:216 msgid "Username" msgstr "" @@ -2884,11 +2907,11 @@ msgstr "" msgid "Users filter mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1050 +#: htdocs/luci-static/resources/view/fchomo/node.js:1071 msgid "V2ray HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +#: htdocs/luci-static/resources/view/fchomo/node.js:1076 msgid "V2ray HTTPUpgrade fast open" msgstr "" @@ -2902,9 +2925,9 @@ msgstr "" msgid "VMess" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1302 -#: htdocs/luci-static/resources/view/fchomo/node.js:1587 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:306 +#: htdocs/luci-static/resources/view/fchomo/node.js:1319 +#: htdocs/luci-static/resources/view/fchomo/node.js:1604 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:304 msgid "Value" msgstr "" @@ -2912,18 +2935,18 @@ msgstr "" msgid "Verify if given" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:418 -#: htdocs/luci-static/resources/view/fchomo/node.js:692 -#: htdocs/luci-static/resources/view/fchomo/server.js:530 +#: htdocs/luci-static/resources/view/fchomo/node.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:713 +#: htdocs/luci-static/resources/view/fchomo/server.js:516 msgid "Version" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:700 +#: htdocs/luci-static/resources/view/fchomo/node.js:721 msgid "Version hint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:221 -#: htdocs/luci-static/resources/view/fchomo/server.js:205 +#: htdocs/luci-static/resources/view/fchomo/node.js:223 +#: htdocs/luci-static/resources/view/fchomo/server.js:172 msgid "Vless Encryption fields" msgstr "" @@ -2935,16 +2958,23 @@ msgstr "" msgid "Warning" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:972 -#: htdocs/luci-static/resources/view/fchomo/node.js:983 -#: htdocs/luci-static/resources/view/fchomo/node.js:988 -#: htdocs/luci-static/resources/view/fchomo/server.js:947 -#: htdocs/luci-static/resources/view/fchomo/server.js:958 -#: htdocs/luci-static/resources/view/fchomo/server.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:993 +#: htdocs/luci-static/resources/view/fchomo/node.js:1004 +#: htdocs/luci-static/resources/view/fchomo/node.js:1009 +#: htdocs/luci-static/resources/view/fchomo/server.js:933 +#: htdocs/luci-static/resources/view/fchomo/server.js:944 +#: htdocs/luci-static/resources/view/fchomo/server.js:949 msgid "WebSocket" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:165 +#: htdocs/luci-static/resources/view/fchomo/node.js:425 +#: htdocs/luci-static/resources/view/fchomo/server.js:403 +msgid "" +"When disabled, downlink ciphertext is split into 6-bit segments, reusing the " +"original padding pool and obfuscate type to reduce downlink overhead." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:132 msgid "When used as a server, HomeProxy is a better choice." msgstr "" @@ -2956,27 +2986,27 @@ msgstr "" msgid "WireGuard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:617 +#: htdocs/luci-static/resources/view/fchomo/node.js:638 msgid "WireGuard peer public key." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:624 +#: htdocs/luci-static/resources/view/fchomo/node.js:645 msgid "WireGuard pre-shared key." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:630 msgid "WireGuard requires base64-encoded private keys." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:593 +#: htdocs/luci-static/resources/view/fchomo/server.js:579 msgid "XOR mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:590 +#: htdocs/luci-static/resources/view/fchomo/node.js:611 msgid "Xudp (Xray-core)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:285 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:283 msgid "Yaml text" msgstr "" @@ -2984,14 +3014,14 @@ msgstr "" msgid "YouTube" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1500 +#: htdocs/luci-static/resources/fchomo.js:1550 msgid "Your %s was successfully uploaded. Size: %sB." msgstr "" #: htdocs/luci-static/resources/fchomo.js:289 #: htdocs/luci-static/resources/fchomo.js:302 #: htdocs/luci-static/resources/fchomo.js:307 -#: htdocs/luci-static/resources/view/fchomo/node.js:570 +#: htdocs/luci-static/resources/view/fchomo/node.js:591 msgid "aes-128-gcm" msgstr "" @@ -3004,18 +3034,18 @@ msgstr "" msgid "aes-256-gcm" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:567 +#: htdocs/luci-static/resources/view/fchomo/node.js:588 msgid "auto" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:444 -#: htdocs/luci-static/resources/view/fchomo/server.js:435 +#: htdocs/luci-static/resources/view/fchomo/node.js:465 +#: htdocs/luci-static/resources/view/fchomo/server.js:421 msgid "bbr" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:901 -#: htdocs/luci-static/resources/view/fchomo/server.js:804 -#: htdocs/luci-static/resources/view/fchomo/server.js:844 +#: htdocs/luci-static/resources/view/fchomo/node.js:922 +#: htdocs/luci-static/resources/view/fchomo/server.js:790 +#: htdocs/luci-static/resources/view/fchomo/server.js:830 msgid "certificate" msgstr "" @@ -3025,17 +3055,17 @@ msgstr "" msgid "chacha20-ietf-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:571 +#: htdocs/luci-static/resources/view/fchomo/node.js:592 msgid "chacha20-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:442 -#: htdocs/luci-static/resources/view/fchomo/server.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:463 +#: htdocs/luci-static/resources/view/fchomo/server.js:419 msgid "cubic" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:545 -#: htdocs/luci-static/resources/view/fchomo/server.js:576 +#: htdocs/luci-static/resources/view/fchomo/server.js:531 +#: htdocs/luci-static/resources/view/fchomo/server.js:562 msgid "decryption" msgstr "" @@ -3043,27 +3073,27 @@ msgstr "" msgid "dnsmasq selects upstream on its own. (may affect CDN accuracy)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1445 +#: htdocs/luci-static/resources/view/fchomo/node.js:1462 msgid "down" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:732 -#: htdocs/luci-static/resources/view/fchomo/node.js:755 -#: htdocs/luci-static/resources/view/fchomo/server.js:580 +#: htdocs/luci-static/resources/view/fchomo/node.js:753 +#: htdocs/luci-static/resources/view/fchomo/node.js:776 +#: htdocs/luci-static/resources/view/fchomo/server.js:566 msgid "encryption" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:971 -#: htdocs/luci-static/resources/view/fchomo/node.js:982 -#: htdocs/luci-static/resources/view/fchomo/node.js:987 -#: htdocs/luci-static/resources/view/fchomo/server.js:946 -#: htdocs/luci-static/resources/view/fchomo/server.js:957 -#: htdocs/luci-static/resources/view/fchomo/server.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:992 +#: htdocs/luci-static/resources/view/fchomo/node.js:1003 +#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/server.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:943 +#: htdocs/luci-static/resources/view/fchomo/server.js:948 msgid "gRPC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 -#: htdocs/luci-static/resources/view/fchomo/server.js:981 +#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +#: htdocs/luci-static/resources/view/fchomo/server.js:967 msgid "gRPC service name" msgstr "" @@ -3071,11 +3101,11 @@ msgstr "" msgid "gVisor" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1071 +#: htdocs/luci-static/resources/view/fchomo/node.js:1092 msgid "h2mux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:745 +#: htdocs/luci-static/resources/view/fchomo/server.js:731 msgid "least one keypair required" msgstr "" @@ -3083,13 +3113,13 @@ msgstr "" msgid "metacubexd" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:883 -#: htdocs/luci-static/resources/view/fchomo/client.js:1122 -#: htdocs/luci-static/resources/view/fchomo/client.js:1218 -#: htdocs/luci-static/resources/view/fchomo/client.js:1359 -#: htdocs/luci-static/resources/view/fchomo/client.js:1581 -#: htdocs/luci-static/resources/view/fchomo/node.js:1268 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:226 +#: htdocs/luci-static/resources/view/fchomo/client.js:889 +#: htdocs/luci-static/resources/view/fchomo/client.js:1124 +#: htdocs/luci-static/resources/view/fchomo/client.js:1216 +#: htdocs/luci-static/resources/view/fchomo/client.js:1353 +#: htdocs/luci-static/resources/view/fchomo/client.js:1571 +#: htdocs/luci-static/resources/view/fchomo/node.js:1285 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:224 msgid "mihomo config" msgstr "" @@ -3097,35 +3127,35 @@ msgstr "" msgid "mlkem768x25519plus" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1137 -#: htdocs/luci-static/resources/view/fchomo/node.js:1423 +#: htdocs/luci-static/resources/view/fchomo/node.js:1158 +#: htdocs/luci-static/resources/view/fchomo/node.js:1440 msgid "mpTCP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:443 -#: htdocs/luci-static/resources/view/fchomo/server.js:434 +#: htdocs/luci-static/resources/view/fchomo/node.js:464 +#: htdocs/luci-static/resources/view/fchomo/server.js:420 msgid "new_reno" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:749 +#: htdocs/luci-static/resources/view/fchomo/client.js:759 msgid "no-resolve" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1251 -#: htdocs/luci-static/resources/fchomo.js:1346 -#: htdocs/luci-static/resources/fchomo.js:1381 -#: htdocs/luci-static/resources/fchomo.js:1393 +#: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1396 +#: htdocs/luci-static/resources/fchomo.js:1431 +#: htdocs/luci-static/resources/fchomo.js:1443 msgid "non-empty value" msgstr "" #: htdocs/luci-static/resources/fchomo.js:287 #: htdocs/luci-static/resources/fchomo.js:301 #: htdocs/luci-static/resources/fchomo.js:313 -#: htdocs/luci-static/resources/view/fchomo/node.js:568 -#: htdocs/luci-static/resources/view/fchomo/node.js:588 -#: htdocs/luci-static/resources/view/fchomo/node.js:661 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:302 -#: htdocs/luci-static/resources/view/fchomo/server.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:589 +#: htdocs/luci-static/resources/view/fchomo/node.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:682 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:300 +#: htdocs/luci-static/resources/view/fchomo/server.js:497 msgid "none" msgstr "" @@ -3133,7 +3163,7 @@ msgstr "" msgid "not found" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:900 +#: htdocs/luci-static/resources/view/fchomo/client.js:906 msgid "not included \",\"" msgstr "" @@ -3141,20 +3171,20 @@ msgstr "" msgid "null" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:662 +#: htdocs/luci-static/resources/view/fchomo/node.js:683 msgid "obfs-simple" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1403 +#: htdocs/luci-static/resources/view/fchomo/node.js:1420 msgid "override.proxy-name" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:589 +#: htdocs/luci-static/resources/view/fchomo/node.js:610 msgid "packet addr (v2ray-core v5+)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:915 -#: htdocs/luci-static/resources/view/fchomo/server.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:936 +#: htdocs/luci-static/resources/view/fchomo/server.js:805 msgid "private key" msgstr "" @@ -3162,33 +3192,33 @@ msgstr "" msgid "razord-meta" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1055 #: htdocs/luci-static/resources/view/fchomo/client.js:1061 +#: htdocs/luci-static/resources/view/fchomo/client.js:1067 msgid "requires front-end adaptation using the API." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:666 +#: htdocs/luci-static/resources/view/fchomo/node.js:687 msgid "restls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:210 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:208 msgid "rule-set" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:665 -#: htdocs/luci-static/resources/view/fchomo/server.js:512 +#: htdocs/luci-static/resources/view/fchomo/node.js:686 +#: htdocs/luci-static/resources/view/fchomo/server.js:498 msgid "shadow-tls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1069 +#: htdocs/luci-static/resources/view/fchomo/node.js:1090 msgid "smux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:731 +#: htdocs/luci-static/resources/view/fchomo/client.js:741 msgid "src" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:329 +#: htdocs/luci-static/resources/view/fchomo/server.js:296 msgid "sudoku-keypair" msgstr "" @@ -3204,62 +3234,62 @@ msgstr "" msgid "unique identifier" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1402 +#: htdocs/luci-static/resources/fchomo.js:1452 msgid "unique value" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1439 +#: htdocs/luci-static/resources/view/fchomo/node.js:1456 msgid "up" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:419 -#: htdocs/luci-static/resources/view/fchomo/node.js:463 -#: htdocs/luci-static/resources/view/fchomo/node.js:693 -#: htdocs/luci-static/resources/view/fchomo/node.js:725 -#: htdocs/luci-static/resources/view/fchomo/server.js:531 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 +#: htdocs/luci-static/resources/view/fchomo/node.js:484 +#: htdocs/luci-static/resources/view/fchomo/node.js:714 +#: htdocs/luci-static/resources/view/fchomo/node.js:746 +#: htdocs/luci-static/resources/view/fchomo/server.js:517 msgid "v1" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:420 -#: htdocs/luci-static/resources/view/fchomo/node.js:694 -#: htdocs/luci-static/resources/view/fchomo/node.js:726 -#: htdocs/luci-static/resources/view/fchomo/server.js:532 +#: htdocs/luci-static/resources/view/fchomo/node.js:441 +#: htdocs/luci-static/resources/view/fchomo/node.js:715 +#: htdocs/luci-static/resources/view/fchomo/node.js:747 +#: htdocs/luci-static/resources/view/fchomo/server.js:518 msgid "v2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:421 -#: htdocs/luci-static/resources/view/fchomo/node.js:695 -#: htdocs/luci-static/resources/view/fchomo/server.js:533 +#: htdocs/luci-static/resources/view/fchomo/node.js:442 +#: htdocs/luci-static/resources/view/fchomo/node.js:716 +#: htdocs/luci-static/resources/view/fchomo/server.js:519 msgid "v3" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1298 -#: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1348 +#: htdocs/luci-static/resources/fchomo.js:1351 msgid "valid JSON format" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:875 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 msgid "valid SHA256 string with %d characters" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1323 -#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1373 +#: htdocs/luci-static/resources/fchomo.js:1376 msgid "valid URL" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1336 +#: htdocs/luci-static/resources/fchomo.js:1386 msgid "valid base64 key with %d characters" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1383 +#: htdocs/luci-static/resources/fchomo.js:1433 msgid "valid key length with %d characters" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1261 +#: htdocs/luci-static/resources/fchomo.js:1311 msgid "valid port value" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1311 +#: htdocs/luci-static/resources/fchomo.js:1361 msgid "valid uuid" msgstr "" @@ -3279,7 +3309,7 @@ msgstr "" msgid "yacd-meta" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1070 +#: htdocs/luci-static/resources/view/fchomo/node.js:1091 msgid "yamux" msgstr "" @@ -3287,10 +3317,10 @@ msgstr "" msgid "zashboard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:569 +#: htdocs/luci-static/resources/view/fchomo/node.js:590 msgid "zero" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1085 +#: htdocs/luci-static/resources/fchomo.js:1135 msgid "🡇" msgstr "" diff --git a/small/luci-app-fchomo/po/zh_Hans/fchomo.po b/small/luci-app-fchomo/po/zh_Hans/fchomo.po index 77c997fcfd..04a154e198 100644 --- a/small/luci-app-fchomo/po/zh_Hans/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hans/fchomo.po @@ -12,36 +12,35 @@ msgstr "" msgid "%s log" msgstr "%s 日志" -#: htdocs/luci-static/resources/fchomo.js:594 -#: htdocs/luci-static/resources/view/fchomo/client.js:237 -#: htdocs/luci-static/resources/view/fchomo/client.js:267 -#: htdocs/luci-static/resources/view/fchomo/client.js:363 +#: htdocs/luci-static/resources/fchomo.js:558 +#: htdocs/luci-static/resources/fchomo.js:561 +#: htdocs/luci-static/resources/view/fchomo/client.js:293 msgid "(Imported)" msgstr "(已导入)" #: htdocs/luci-static/resources/view/fchomo/global.js:549 #: htdocs/luci-static/resources/view/fchomo/global.js:555 -#: htdocs/luci-static/resources/view/fchomo/node.js:890 -#: htdocs/luci-static/resources/view/fchomo/node.js:896 -#: htdocs/luci-static/resources/view/fchomo/node.js:904 -#: htdocs/luci-static/resources/view/fchomo/node.js:910 -#: htdocs/luci-static/resources/view/fchomo/server.js:822 -#: htdocs/luci-static/resources/view/fchomo/server.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 +#: htdocs/luci-static/resources/view/fchomo/node.js:911 +#: htdocs/luci-static/resources/view/fchomo/node.js:917 +#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/server.js:816 +#: htdocs/luci-static/resources/view/fchomo/server.js:825 msgid "(mTLS)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:924 -#: htdocs/luci-static/resources/view/fchomo/client.js:925 -#: htdocs/luci-static/resources/view/fchomo/client.js:938 -#: htdocs/luci-static/resources/view/fchomo/client.js:939 -#: htdocs/luci-static/resources/view/fchomo/client.js:1151 -#: htdocs/luci-static/resources/view/fchomo/client.js:1622 -#: htdocs/luci-static/resources/view/fchomo/client.js:1623 -#: htdocs/luci-static/resources/view/fchomo/node.js:1591 -#: htdocs/luci-static/resources/view/fchomo/node.js:1597 -#: htdocs/luci-static/resources/view/fchomo/node.js:1611 -#: htdocs/luci-static/resources/view/fchomo/node.js:1617 +#: htdocs/luci-static/resources/view/fchomo/client.js:930 +#: htdocs/luci-static/resources/view/fchomo/client.js:931 +#: htdocs/luci-static/resources/view/fchomo/client.js:944 +#: htdocs/luci-static/resources/view/fchomo/client.js:945 +#: htdocs/luci-static/resources/view/fchomo/client.js:1153 +#: htdocs/luci-static/resources/view/fchomo/client.js:1612 +#: htdocs/luci-static/resources/view/fchomo/client.js:1613 +#: htdocs/luci-static/resources/view/fchomo/node.js:1608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1614 +#: htdocs/luci-static/resources/view/fchomo/node.js:1628 +#: htdocs/luci-static/resources/view/fchomo/node.js:1634 msgid "-- Please choose --" msgstr "-- 请选择 --" @@ -70,15 +69,15 @@ msgstr "" msgid "2022-blake3-chacha20-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:617 +#: htdocs/luci-static/resources/view/fchomo/client.js:627 msgid "0 or 1 only." msgstr "仅限 01。" -#: htdocs/luci-static/resources/view/fchomo/node.js:897 -#: htdocs/luci-static/resources/view/fchomo/node.js:911 -#: htdocs/luci-static/resources/view/fchomo/server.js:800 -#: htdocs/luci-static/resources/view/fchomo/server.js:815 -#: htdocs/luci-static/resources/view/fchomo/server.js:840 +#: htdocs/luci-static/resources/view/fchomo/node.js:918 +#: htdocs/luci-static/resources/view/fchomo/node.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:786 +#: htdocs/luci-static/resources/view/fchomo/server.js:801 +#: htdocs/luci-static/resources/view/fchomo/server.js:826 msgid "Save your configuration before uploading files!" msgstr "上传文件前请先保存配置!" @@ -135,56 +134,56 @@ msgstr "ASN 版本" msgid "Access Control" msgstr "访问控制" -#: htdocs/luci-static/resources/view/fchomo/client.js:1551 +#: htdocs/luci-static/resources/view/fchomo/client.js:1545 msgid "Add a DNS policy" msgstr "新增 DNS 策略" -#: htdocs/luci-static/resources/view/fchomo/client.js:1314 +#: htdocs/luci-static/resources/view/fchomo/client.js:1312 msgid "Add a DNS server" msgstr "新增 DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/node.js:215 +#: htdocs/luci-static/resources/view/fchomo/node.js:217 msgid "Add a Node" msgstr "新增 节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 +#: htdocs/luci-static/resources/view/fchomo/node.js:1198 msgid "Add a provider" msgstr "新增 供应商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1567 +#: htdocs/luci-static/resources/view/fchomo/node.js:1584 msgid "Add a proxy chain" msgstr "新增 代理链" -#: htdocs/luci-static/resources/view/fchomo/client.js:819 +#: htdocs/luci-static/resources/view/fchomo/client.js:829 msgid "Add a proxy group" msgstr "新增 代理组" -#: htdocs/luci-static/resources/view/fchomo/client.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:1081 msgid "Add a routing rule" msgstr "新增 路由规则" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:133 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:135 msgid "Add a rule set" msgstr "新增 规则集" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 +#: htdocs/luci-static/resources/view/fchomo/server.js:166 msgid "Add a server" msgstr "新增 服务器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1185 +#: htdocs/luci-static/resources/view/fchomo/client.js:1187 msgid "Add a sub rule" msgstr "新增 子规则" -#: htdocs/luci-static/resources/view/fchomo/node.js:1392 +#: htdocs/luci-static/resources/view/fchomo/node.js:1409 msgid "Add prefix" msgstr "添加前缀" -#: htdocs/luci-static/resources/view/fchomo/node.js:1396 +#: htdocs/luci-static/resources/view/fchomo/node.js:1413 msgid "Add suffix" msgstr "添加后缀" -#: htdocs/luci-static/resources/view/fchomo/client.js:1376 -#: htdocs/luci-static/resources/view/fchomo/client.js:1381 +#: htdocs/luci-static/resources/view/fchomo/client.js:1370 +#: htdocs/luci-static/resources/view/fchomo/client.js:1375 msgid "Address" msgstr "地址" @@ -201,7 +200,7 @@ msgstr "客户端维护的 NAT 映射 的老化时间。
" #: htdocs/luci-static/resources/view/fchomo/global.js:779 #: htdocs/luci-static/resources/view/fchomo/global.js:842 -#: htdocs/luci-static/resources/view/fchomo/global.js:861 +#: htdocs/luci-static/resources/view/fchomo/global.js:876 msgid "All allowed" msgstr "允许所有" @@ -216,7 +215,7 @@ msgid "" msgstr "" "允许从私有网络访问。
要从公共网站访问私有网络上的 API,则必须启用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:630 +#: htdocs/luci-static/resources/view/fchomo/node.js:651 msgid "Allowed IPs" msgstr "允许的 IP" @@ -228,8 +227,8 @@ msgstr "已是最新版本。" msgid "Already in updating." msgstr "已在更新中。" -#: htdocs/luci-static/resources/view/fchomo/node.js:559 -#: htdocs/luci-static/resources/view/fchomo/server.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:580 +#: htdocs/luci-static/resources/view/fchomo/server.js:488 msgid "Alter ID" msgstr "额外 ID" @@ -251,11 +250,11 @@ msgstr "作为 dnsmasq 的最优先上游" msgid "As the TOP upstream of dnsmasq." msgstr "作为 dnsmasq 的最优先上游。" -#: htdocs/luci-static/resources/view/fchomo/server.js:452 +#: htdocs/luci-static/resources/view/fchomo/server.js:438 msgid "Auth timeout" msgstr "认证超时" -#: htdocs/luci-static/resources/view/fchomo/node.js:581 +#: htdocs/luci-static/resources/view/fchomo/node.js:602 msgid "Authenticated length" msgstr "认证长度" @@ -263,7 +262,7 @@ msgstr "认证长度" msgid "Auto" msgstr "自动" -#: htdocs/luci-static/resources/view/fchomo/server.js:222 +#: htdocs/luci-static/resources/view/fchomo/server.js:189 msgid "Auto configure firewall" msgstr "自动配置防火墙" @@ -283,27 +282,27 @@ msgstr "百度" msgid "Based on google/gvisor." msgstr "基于 google/gvisor。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:269 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:267 msgid "Behavior" msgstr "行为" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:278 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:292 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:276 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:290 msgid "Binary format only supports domain / ipcidr" msgstr "二进制格式仅支持 domain/ipcidr" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:286 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 msgid "Binary mrs" msgstr "二进制 mrs" #: htdocs/luci-static/resources/view/fchomo/global.js:740 -#: htdocs/luci-static/resources/view/fchomo/node.js:1147 -#: htdocs/luci-static/resources/view/fchomo/node.js:1465 +#: htdocs/luci-static/resources/view/fchomo/node.js:1168 +#: htdocs/luci-static/resources/view/fchomo/node.js:1482 msgid "Bind interface" msgstr "绑定接口" -#: htdocs/luci-static/resources/view/fchomo/node.js:1148 -#: htdocs/luci-static/resources/view/fchomo/node.js:1466 +#: htdocs/luci-static/resources/view/fchomo/node.js:1169 +#: htdocs/luci-static/resources/view/fchomo/node.js:1483 msgid "Bind outbound interface.
" msgstr "绑定出站接口。
" @@ -316,15 +315,15 @@ msgstr "绑定出站流量至指定接口。留空自动检测。
" msgid "Black list" msgstr "黑名单" -#: htdocs/luci-static/resources/view/fchomo/client.js:55 +#: htdocs/luci-static/resources/view/fchomo/client.js:57 msgid "Block DNS queries" msgstr "封锁 DNS 请求" -#: htdocs/luci-static/resources/view/fchomo/client.js:1259 +#: htdocs/luci-static/resources/view/fchomo/client.js:1257 msgid "Bootstrap DNS server" msgstr "引导 DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1266 +#: htdocs/luci-static/resources/view/fchomo/client.js:1264 msgid "Bootstrap DNS server (Node)" msgstr "引导 DNS 服务器 (节点)" @@ -332,7 +331,7 @@ msgstr "引导 DNS 服务器 (节点)" msgid "Bypass CN" msgstr "绕过 CN 流量" -#: htdocs/luci-static/resources/view/fchomo/global.js:862 +#: htdocs/luci-static/resources/view/fchomo/global.js:877 msgid "Bypass DSCP" msgstr "绕过 DSCP" @@ -348,21 +347,21 @@ msgstr "CORS 允许私有网络" msgid "CORS allowed origins, * will be used if empty." msgstr "CORS 允许的来源,留空则使用 *。" -#: htdocs/luci-static/resources/fchomo.js:614 +#: htdocs/luci-static/resources/fchomo.js:674 msgid "Cancel" msgstr "取消" -#: htdocs/luci-static/resources/view/fchomo/node.js:869 +#: htdocs/luci-static/resources/view/fchomo/node.js:890 msgid "Cert fingerprint" msgstr "证书指纹" -#: htdocs/luci-static/resources/view/fchomo/node.js:870 +#: htdocs/luci-static/resources/view/fchomo/node.js:891 msgid "" "Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." msgstr "证书指纹。用于实现 SSL证书固定 并防止 MitM。" -#: htdocs/luci-static/resources/view/fchomo/node.js:890 -#: htdocs/luci-static/resources/view/fchomo/server.js:792 +#: htdocs/luci-static/resources/view/fchomo/node.js:911 +#: htdocs/luci-static/resources/view/fchomo/server.js:778 msgid "Certificate path" msgstr "证书路径" @@ -391,14 +390,19 @@ msgstr "大陆 IPv6 库版本" msgid "China list version" msgstr "大陆域名列表版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:324 -#: htdocs/luci-static/resources/view/fchomo/node.js:377 -#: htdocs/luci-static/resources/view/fchomo/node.js:565 -#: htdocs/luci-static/resources/view/fchomo/server.js:302 -#: htdocs/luci-static/resources/view/fchomo/server.js:389 +#: htdocs/luci-static/resources/view/fchomo/node.js:326 +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +#: htdocs/luci-static/resources/view/fchomo/node.js:586 +#: htdocs/luci-static/resources/view/fchomo/server.js:269 +#: htdocs/luci-static/resources/view/fchomo/server.js:356 msgid "Chipher" msgstr "加密方法" +#: htdocs/luci-static/resources/view/fchomo/node.js:388 +#: htdocs/luci-static/resources/view/fchomo/server.js:365 +msgid "Chipher must be enabled if obfuscate downlink is disabled." +msgstr "如果下行链路混淆功能被禁用,则必须启用加密。" + #: htdocs/luci-static/resources/view/fchomo/log.js:119 msgid "Clean log" msgstr "清空日志" @@ -412,28 +416,28 @@ msgstr "" "最新的初始包。" #: htdocs/luci-static/resources/view/fchomo/global.js:556 -#: htdocs/luci-static/resources/view/fchomo/node.js:767 -#: htdocs/luci-static/resources/view/fchomo/node.js:891 -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:621 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/view/fchomo/node.js:788 +#: htdocs/luci-static/resources/view/fchomo/node.js:912 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 +#: htdocs/luci-static/resources/view/fchomo/server.js:607 +#: htdocs/luci-static/resources/view/fchomo/server.js:817 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 msgid "Client" msgstr "客户端" -#: htdocs/luci-static/resources/view/fchomo/server.js:830 +#: htdocs/luci-static/resources/view/fchomo/server.js:816 msgid "Client Auth Certificate path" msgstr "客户端认证证书路径" -#: htdocs/luci-static/resources/view/fchomo/server.js:822 +#: htdocs/luci-static/resources/view/fchomo/server.js:808 msgid "Client Auth type" msgstr "客户端认证类型" -#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/node.js:952 msgid "Client fingerprint" msgstr "客户端指纹" -#: htdocs/luci-static/resources/view/fchomo/server.js:385 +#: htdocs/luci-static/resources/view/fchomo/server.js:352 msgid "Client key" msgstr "客户端密钥" @@ -450,16 +454,16 @@ msgstr "收集数据中..." msgid "Common ports (bypass P2P traffic)" msgstr "常用端口(绕过 P2P 流量)" -#: htdocs/luci-static/resources/fchomo.js:1195 +#: htdocs/luci-static/resources/fchomo.js:1245 msgid "Complete" msgstr "完成" -#: htdocs/luci-static/resources/view/fchomo/node.js:1412 +#: htdocs/luci-static/resources/view/fchomo/node.js:1429 msgid "Configuration Items" msgstr "配置项" -#: htdocs/luci-static/resources/view/fchomo/node.js:439 -#: htdocs/luci-static/resources/view/fchomo/server.js:430 +#: htdocs/luci-static/resources/view/fchomo/node.js:460 +#: htdocs/luci-static/resources/view/fchomo/server.js:416 msgid "Congestion controller" msgstr "拥塞控制器" @@ -467,20 +471,20 @@ msgstr "拥塞控制器" msgid "Connection check" msgstr "连接检查" -#: htdocs/luci-static/resources/view/fchomo/server.js:35 +#: htdocs/luci-static/resources/fchomo.js:543 msgid "Content copied to clipboard!" msgstr "内容已复制到剪贴板!" -#: htdocs/luci-static/resources/view/fchomo/client.js:598 -#: htdocs/luci-static/resources/view/fchomo/node.js:1322 -#: htdocs/luci-static/resources/view/fchomo/node.js:1348 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:326 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:352 +#: htdocs/luci-static/resources/view/fchomo/client.js:608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 +#: htdocs/luci-static/resources/view/fchomo/node.js:1365 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:324 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:350 msgid "Content will not be verified, Please make sure you enter it correctly." msgstr "内容将不会被验证,请确保输入正确。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1321 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:325 +#: htdocs/luci-static/resources/view/fchomo/node.js:1338 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:323 msgid "Contents" msgstr "内容" @@ -488,7 +492,7 @@ msgstr "内容" msgid "Contents have been saved." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:37 +#: htdocs/luci-static/resources/fchomo.js:545 msgid "Copy" msgstr "复制" @@ -500,18 +504,23 @@ msgstr "核心版本" msgid "Cron expression" msgstr "Cron 表达式" -#: htdocs/luci-static/resources/view/fchomo/global.js:880 +#: htdocs/luci-static/resources/view/fchomo/global.js:895 msgid "Custom Direct List" msgstr "自定义直连列表" -#: htdocs/luci-static/resources/view/fchomo/node.js:1383 +#: htdocs/luci-static/resources/view/fchomo/node.js:1400 msgid "Custom HTTP header." msgstr "自定义 HTTP header。" -#: htdocs/luci-static/resources/view/fchomo/global.js:898 +#: htdocs/luci-static/resources/view/fchomo/global.js:913 msgid "Custom Proxy List" msgstr "自定义代理列表" +#: htdocs/luci-static/resources/view/fchomo/node.js:401 +#: htdocs/luci-static/resources/view/fchomo/server.js:378 +msgid "Custom byte layout" +msgstr "自定义字节布局" + #: htdocs/luci-static/resources/view/fchomo/hosts.js:28 msgid "" "Custom internal hosts. Support yaml or json format." @@ -521,8 +530,8 @@ msgstr "自定义内部 hosts。支持 yamljson msgid "DIRECT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1542 -#: htdocs/luci-static/resources/view/fchomo/client.js:1551 +#: htdocs/luci-static/resources/view/fchomo/client.js:1536 +#: htdocs/luci-static/resources/view/fchomo/client.js:1545 msgid "DNS policy" msgstr "DNS 策略" @@ -530,18 +539,18 @@ msgstr "DNS 策略" msgid "DNS port" msgstr " DNS 端口" -#: htdocs/luci-static/resources/view/fchomo/client.js:1305 -#: htdocs/luci-static/resources/view/fchomo/client.js:1314 -#: htdocs/luci-static/resources/view/fchomo/client.js:1635 -#: htdocs/luci-static/resources/view/fchomo/node.js:654 +#: htdocs/luci-static/resources/view/fchomo/client.js:1303 +#: htdocs/luci-static/resources/view/fchomo/client.js:1312 +#: htdocs/luci-static/resources/view/fchomo/client.js:1625 +#: htdocs/luci-static/resources/view/fchomo/node.js:675 msgid "DNS server" msgstr "DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1245 +#: htdocs/luci-static/resources/view/fchomo/client.js:1243 msgid "DNS settings" msgstr "DNS 设置" -#: htdocs/luci-static/resources/view/fchomo/global.js:865 +#: htdocs/luci-static/resources/view/fchomo/global.js:880 msgid "DSCP list" msgstr "DSCP 列表" @@ -553,32 +562,32 @@ msgstr "面板版本" msgid "Debug" msgstr "调试" -#: htdocs/luci-static/resources/view/fchomo/client.js:53 +#: htdocs/luci-static/resources/view/fchomo/client.js:55 msgid "Default DNS (issued by WAN)" msgstr "默认 DNS(由 WAN 下发)" -#: htdocs/luci-static/resources/view/fchomo/client.js:1280 +#: htdocs/luci-static/resources/view/fchomo/client.js:1278 msgid "Default DNS server" msgstr "默认 DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/node.js:631 +#: htdocs/luci-static/resources/view/fchomo/node.js:652 msgid "Destination addresses allowed to be forwarded via Wireguard." msgstr "允许通过 WireGuard 转发的目的地址" -#: htdocs/luci-static/resources/view/fchomo/node.js:1590 +#: htdocs/luci-static/resources/view/fchomo/node.js:1607 msgid "Destination provider" msgstr "落地供应商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1596 +#: htdocs/luci-static/resources/view/fchomo/node.js:1613 msgid "Destination proxy node" msgstr "落地代理节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:225 +#: htdocs/luci-static/resources/view/fchomo/node.js:227 msgid "Dial fields" msgstr "拨号字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:1603 -#: htdocs/luci-static/resources/view/fchomo/node.js:1623 +#: htdocs/luci-static/resources/view/fchomo/node.js:1620 +#: htdocs/luci-static/resources/view/fchomo/node.js:1640 msgid "Different chain head/tail" msgstr "不同的链头/链尾" @@ -599,8 +608,8 @@ msgid "Direct MAC-s" msgstr "直连 MAC 地址" #: htdocs/luci-static/resources/view/fchomo/global.js:403 -#: htdocs/luci-static/resources/view/fchomo/node.js:290 -#: htdocs/luci-static/resources/view/fchomo/server.js:282 +#: htdocs/luci-static/resources/view/fchomo/node.js:292 +#: htdocs/luci-static/resources/view/fchomo/server.js:249 msgid "Disable" msgstr "禁用" @@ -616,11 +625,11 @@ msgstr "禁用 quic-go 的 通用分段卸载(GSO)" msgid "Disable ICMP Forwarding" msgstr "禁用 ICMP 转发" -#: htdocs/luci-static/resources/view/fchomo/node.js:818 +#: htdocs/luci-static/resources/view/fchomo/node.js:839 msgid "Disable SNI" msgstr "禁用 SNI" -#: htdocs/luci-static/resources/view/fchomo/client.js:967 +#: htdocs/luci-static/resources/view/fchomo/client.js:973 msgid "Disable UDP" msgstr "禁用 UDP" @@ -628,15 +637,15 @@ msgstr "禁用 UDP" msgid "Disable safe path check" msgstr "禁用安全路径检查" -#: htdocs/luci-static/resources/view/fchomo/client.js:1508 +#: htdocs/luci-static/resources/view/fchomo/client.js:1502 msgid "Discard A responses" msgstr "丢弃 A 响应" -#: htdocs/luci-static/resources/view/fchomo/client.js:1524 +#: htdocs/luci-static/resources/view/fchomo/client.js:1518 msgid "Discard AAAA responses" msgstr "丢弃 AAAA 响应" -#: htdocs/luci-static/resources/view/fchomo/client.js:750 +#: htdocs/luci-static/resources/view/fchomo/client.js:760 msgid "" "Do not resolve the domain connection to IP for this match.
Only works " "for pure domain inbound connections without DNS resolution. e.g., socks5h" @@ -644,37 +653,37 @@ msgstr "" "不要将域名连接解析为 IP 以进行此次匹配。
仅对未经 DNS 解析的纯域名入站连" "接有效。例如,socks5h" -#: htdocs/luci-static/resources/view/fchomo/client.js:1605 -#: htdocs/luci-static/resources/view/fchomo/client.js:1610 -#: htdocs/luci-static/resources/view/fchomo/client.js:1675 -#: htdocs/luci-static/resources/view/fchomo/client.js:1682 -#: htdocs/luci-static/resources/view/fchomo/client.js:1684 +#: htdocs/luci-static/resources/view/fchomo/client.js:1595 +#: htdocs/luci-static/resources/view/fchomo/client.js:1600 +#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1672 +#: htdocs/luci-static/resources/view/fchomo/client.js:1674 msgid "Domain" msgstr "域名" -#: htdocs/luci-static/resources/view/fchomo/node.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:840 msgid "Donot send server name in ClientHello." msgstr "不要在 ClientHello 中发送服务器名称。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1456 -#: htdocs/luci-static/resources/view/fchomo/node.js:883 -#: htdocs/luci-static/resources/view/fchomo/node.js:1452 +#: htdocs/luci-static/resources/view/fchomo/client.js:1450 +#: htdocs/luci-static/resources/view/fchomo/node.js:904 +#: htdocs/luci-static/resources/view/fchomo/node.js:1469 msgid "Donot verifying server certificate." msgstr "不验证服务器证书。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1147 msgid "Download bandwidth" msgstr "下载带宽" -#: htdocs/luci-static/resources/view/fchomo/node.js:1127 +#: htdocs/luci-static/resources/view/fchomo/node.js:1148 msgid "Download bandwidth in Mbps." msgstr "下载带宽(单位:Mbps)。" -#: htdocs/luci-static/resources/fchomo.js:1080 +#: htdocs/luci-static/resources/fchomo.js:1130 msgid "Download failed: %s" msgstr "下载失败: %s" -#: htdocs/luci-static/resources/fchomo.js:1078 +#: htdocs/luci-static/resources/fchomo.js:1128 msgid "Download successful." msgstr "下载成功。" @@ -682,20 +691,20 @@ msgstr "下载成功。" msgid "Dual stack" msgstr "双栈" -#: htdocs/luci-static/resources/view/fchomo/node.js:924 -#: htdocs/luci-static/resources/view/fchomo/server.js:888 +#: htdocs/luci-static/resources/view/fchomo/node.js:945 +#: htdocs/luci-static/resources/view/fchomo/server.js:874 msgid "ECH config" msgstr "ECH 配置" -#: htdocs/luci-static/resources/view/fchomo/server.js:847 +#: htdocs/luci-static/resources/view/fchomo/server.js:833 msgid "ECH key" msgstr "ECH 密钥" -#: htdocs/luci-static/resources/view/fchomo/client.js:1490 +#: htdocs/luci-static/resources/view/fchomo/client.js:1484 msgid "ECS override" msgstr "强制覆盖 ECS" -#: htdocs/luci-static/resources/view/fchomo/client.js:1474 +#: htdocs/luci-static/resources/view/fchomo/client.js:1468 msgid "EDNS Client Subnet" msgstr "" @@ -703,24 +712,24 @@ msgstr "" msgid "ETag support" msgstr "ETag 支持" -#: htdocs/luci-static/resources/view/fchomo/node.js:1039 +#: htdocs/luci-static/resources/view/fchomo/node.js:1060 msgid "Early Data first packet length limit." msgstr "前置数据长度阈值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1045 +#: htdocs/luci-static/resources/view/fchomo/node.js:1066 msgid "Early Data header name" msgstr "前置数据标头" -#: htdocs/luci-static/resources/view/fchomo/node.js:201 +#: htdocs/luci-static/resources/view/fchomo/node.js:203 msgid "Edit node" msgstr "编辑节点" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:124 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:126 msgid "Edit ruleset" msgstr "编辑规则集" -#: htdocs/luci-static/resources/view/fchomo/node.js:1319 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:323 +#: htdocs/luci-static/resources/view/fchomo/node.js:1336 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:321 msgid "Editer" msgstr "编辑器" @@ -728,25 +737,25 @@ msgstr "编辑器" msgid "Eliminate encryption header characteristics" msgstr "消除加密头特征" -#: htdocs/luci-static/resources/view/fchomo/client.js:809 -#: htdocs/luci-static/resources/view/fchomo/client.js:906 -#: htdocs/luci-static/resources/view/fchomo/client.js:1135 -#: htdocs/luci-static/resources/view/fchomo/client.js:1231 -#: htdocs/luci-static/resources/view/fchomo/client.js:1372 -#: htdocs/luci-static/resources/view/fchomo/client.js:1594 +#: htdocs/luci-static/resources/view/fchomo/client.js:819 +#: htdocs/luci-static/resources/view/fchomo/client.js:912 +#: htdocs/luci-static/resources/view/fchomo/client.js:1137 +#: htdocs/luci-static/resources/view/fchomo/client.js:1229 +#: htdocs/luci-static/resources/view/fchomo/client.js:1366 +#: htdocs/luci-static/resources/view/fchomo/client.js:1584 #: htdocs/luci-static/resources/view/fchomo/global.js:401 #: htdocs/luci-static/resources/view/fchomo/global.js:686 -#: htdocs/luci-static/resources/view/fchomo/node.js:232 -#: htdocs/luci-static/resources/view/fchomo/node.js:1292 -#: htdocs/luci-static/resources/view/fchomo/node.js:1488 -#: htdocs/luci-static/resources/view/fchomo/node.js:1577 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:251 -#: htdocs/luci-static/resources/view/fchomo/server.js:190 -#: htdocs/luci-static/resources/view/fchomo/server.js:217 +#: htdocs/luci-static/resources/view/fchomo/node.js:234 +#: htdocs/luci-static/resources/view/fchomo/node.js:1309 +#: htdocs/luci-static/resources/view/fchomo/node.js:1505 +#: htdocs/luci-static/resources/view/fchomo/node.js:1594 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:249 +#: htdocs/luci-static/resources/view/fchomo/server.js:157 +#: htdocs/luci-static/resources/view/fchomo/server.js:184 msgid "Enable" msgstr "启用" -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:495 msgid "" "Enable 0-RTT QUIC connection handshake on the client side. This is not " "impacting much on the performance, as the protocol is fully multiplexed.
强烈建议禁用此功能,因为它容易受到重放攻击。" -#: htdocs/luci-static/resources/view/fchomo/node.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:494 msgid "Enable 0-RTT handshake" msgstr "启用 0-RTT 握手" @@ -767,44 +776,49 @@ msgstr "" "为出站连接启用
IP4P 转换" -#: htdocs/luci-static/resources/view/fchomo/node.js:918 +#: htdocs/luci-static/resources/view/fchomo/node.js:939 msgid "Enable ECH" msgstr "启用 ECH" -#: htdocs/luci-static/resources/view/fchomo/node.js:1114 +#: htdocs/luci-static/resources/view/fchomo/node.js:1135 msgid "Enable TCP Brutal" msgstr "启用 TCP Brutal" -#: htdocs/luci-static/resources/view/fchomo/node.js:1115 +#: htdocs/luci-static/resources/view/fchomo/node.js:1136 msgid "Enable TCP Brutal congestion control algorithm" msgstr "启用 TCP Brutal 拥塞控制算法。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1103 +#: htdocs/luci-static/resources/view/fchomo/node.js:1124 msgid "Enable multiplexing only for TCP." msgstr "仅为 TCP 启用多路复用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1097 +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +#: htdocs/luci-static/resources/view/fchomo/server.js:402 +msgid "Enable obfuscate for downlink" +msgstr "启用下行链路混淆" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1118 msgid "Enable padding" msgstr "启用填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1108 +#: htdocs/luci-static/resources/view/fchomo/node.js:1129 msgid "Enable statistic" msgstr "启用统计" -#: htdocs/luci-static/resources/view/fchomo/node.js:719 -#: htdocs/luci-static/resources/view/fchomo/node.js:1434 +#: htdocs/luci-static/resources/view/fchomo/node.js:740 +#: htdocs/luci-static/resources/view/fchomo/node.js:1451 msgid "" "Enable the SUoT protocol, requires server support. Conflict with Multiplex." msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。" -#: htdocs/luci-static/resources/view/fchomo/node.js:296 -#: htdocs/luci-static/resources/view/fchomo/server.js:288 +#: htdocs/luci-static/resources/view/fchomo/node.js:298 +#: htdocs/luci-static/resources/view/fchomo/server.js:255 msgid "" "Enabling obfuscation will make the server incompatible with standard QUIC " "connections, losing the ability to masquerade with HTTP/3." msgstr "启用混淆将使服务器与标准的 QUIC 连接不兼容,失去 HTTP/3 伪装的能力。" -#: htdocs/luci-static/resources/view/fchomo/server.js:584 +#: htdocs/luci-static/resources/view/fchomo/server.js:570 msgid "Encryption method" msgstr "加密方法" @@ -812,8 +826,8 @@ msgstr "加密方法" msgid "Endpoint-Independent NAT" msgstr "端点独立 NAT" -#: htdocs/luci-static/resources/view/fchomo/client.js:650 -#: htdocs/luci-static/resources/view/fchomo/client.js:1627 +#: htdocs/luci-static/resources/view/fchomo/client.js:660 +#: htdocs/luci-static/resources/view/fchomo/client.js:1617 msgid "Entry" msgstr "条目" @@ -821,17 +835,17 @@ msgstr "条目" msgid "Error" msgstr "错误" -#: htdocs/luci-static/resources/view/fchomo/client.js:1011 +#: htdocs/luci-static/resources/view/fchomo/client.js:1017 msgid "" "Exceeding this triggers a forced health check. 5 will be used " "if empty." msgstr "超过此限制将会触发强制健康检查。留空则使用 5。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1546 +#: htdocs/luci-static/resources/view/fchomo/node.js:1563 msgid "Exclude matched node types." msgstr "排除匹配的节点类型。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +#: htdocs/luci-static/resources/view/fchomo/client.js:1054 msgid "" "Exclude matched node types. Available types see here." @@ -839,8 +853,8 @@ msgstr "" "排除匹配的节点类型。可用类型请参见此处。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1043 -#: htdocs/luci-static/resources/view/fchomo/node.js:1539 +#: htdocs/luci-static/resources/view/fchomo/client.js:1049 +#: htdocs/luci-static/resources/view/fchomo/node.js:1556 msgid "Exclude nodes that meet keywords or regexps." msgstr "排除匹配关键词或表达式的节点。" @@ -848,58 +862,60 @@ msgstr "排除匹配关键词或表达式的节点。" msgid "Expand/Collapse result" msgstr "展开/收起 结果" -#: htdocs/luci-static/resources/view/fchomo/client.js:1003 -#: htdocs/luci-static/resources/view/fchomo/node.js:1524 +#: htdocs/luci-static/resources/view/fchomo/client.js:1009 +#: htdocs/luci-static/resources/view/fchomo/node.js:1541 msgid "Expected HTTP code. 204 will be used if empty." msgstr "预期的 HTTP code。留空则使用 204。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1005 -#: htdocs/luci-static/resources/view/fchomo/node.js:1526 +#: htdocs/luci-static/resources/view/fchomo/client.js:1011 +#: htdocs/luci-static/resources/view/fchomo/node.js:1543 msgid "Expected status" msgstr "预期状态" #: htdocs/luci-static/resources/fchomo.js:393 #: htdocs/luci-static/resources/fchomo.js:396 #: htdocs/luci-static/resources/fchomo.js:399 -#: htdocs/luci-static/resources/fchomo.js:1212 -#: htdocs/luci-static/resources/fchomo.js:1220 -#: htdocs/luci-static/resources/fchomo.js:1228 -#: htdocs/luci-static/resources/fchomo.js:1251 -#: htdocs/luci-static/resources/fchomo.js:1254 -#: htdocs/luci-static/resources/fchomo.js:1261 -#: htdocs/luci-static/resources/fchomo.js:1277 -#: htdocs/luci-static/resources/fchomo.js:1286 -#: htdocs/luci-static/resources/fchomo.js:1298 +#: htdocs/luci-static/resources/fchomo.js:1262 +#: htdocs/luci-static/resources/fchomo.js:1270 +#: htdocs/luci-static/resources/fchomo.js:1278 #: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1304 #: htdocs/luci-static/resources/fchomo.js:1311 -#: htdocs/luci-static/resources/fchomo.js:1323 -#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1327 #: htdocs/luci-static/resources/fchomo.js:1336 -#: htdocs/luci-static/resources/fchomo.js:1346 -#: htdocs/luci-static/resources/fchomo.js:1381 -#: htdocs/luci-static/resources/fchomo.js:1383 -#: htdocs/luci-static/resources/fchomo.js:1393 -#: htdocs/luci-static/resources/fchomo.js:1402 -#: htdocs/luci-static/resources/view/fchomo/client.js:66 -#: htdocs/luci-static/resources/view/fchomo/client.js:900 -#: htdocs/luci-static/resources/view/fchomo/client.js:1387 -#: htdocs/luci-static/resources/view/fchomo/global.js:871 -#: htdocs/luci-static/resources/view/fchomo/node.js:790 -#: htdocs/luci-static/resources/view/fchomo/node.js:875 -#: htdocs/luci-static/resources/view/fchomo/node.js:1603 -#: htdocs/luci-static/resources/view/fchomo/node.js:1623 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:278 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:292 -#: htdocs/luci-static/resources/view/fchomo/server.js:613 -#: htdocs/luci-static/resources/view/fchomo/server.js:644 -#: htdocs/luci-static/resources/view/fchomo/server.js:745 +#: htdocs/luci-static/resources/fchomo.js:1348 +#: htdocs/luci-static/resources/fchomo.js:1351 +#: htdocs/luci-static/resources/fchomo.js:1361 +#: htdocs/luci-static/resources/fchomo.js:1373 +#: htdocs/luci-static/resources/fchomo.js:1376 +#: htdocs/luci-static/resources/fchomo.js:1386 +#: htdocs/luci-static/resources/fchomo.js:1396 +#: htdocs/luci-static/resources/fchomo.js:1431 +#: htdocs/luci-static/resources/fchomo.js:1433 +#: htdocs/luci-static/resources/fchomo.js:1443 +#: htdocs/luci-static/resources/fchomo.js:1452 +#: htdocs/luci-static/resources/view/fchomo/client.js:68 +#: htdocs/luci-static/resources/view/fchomo/client.js:906 +#: htdocs/luci-static/resources/view/fchomo/client.js:1381 +#: htdocs/luci-static/resources/view/fchomo/global.js:886 +#: htdocs/luci-static/resources/view/fchomo/node.js:388 +#: htdocs/luci-static/resources/view/fchomo/node.js:811 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 +#: htdocs/luci-static/resources/view/fchomo/node.js:1620 +#: htdocs/luci-static/resources/view/fchomo/node.js:1640 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:276 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:290 +#: htdocs/luci-static/resources/view/fchomo/server.js:365 +#: htdocs/luci-static/resources/view/fchomo/server.js:599 +#: htdocs/luci-static/resources/view/fchomo/server.js:630 +#: htdocs/luci-static/resources/view/fchomo/server.js:731 msgid "Expecting: %s" msgstr "请输入:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:980 -#: htdocs/luci-static/resources/view/fchomo/node.js:987 -#: htdocs/luci-static/resources/view/fchomo/server.js:955 -#: htdocs/luci-static/resources/view/fchomo/server.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:1001 +#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/server.js:941 +#: htdocs/luci-static/resources/view/fchomo/server.js:948 msgid "Expecting: only support %s." msgstr "请输入:仅支援 %s." @@ -907,30 +923,30 @@ msgstr "请输入:仅支援 %s." msgid "Experimental" msgstr "实验性" -#: htdocs/luci-static/resources/view/fchomo/client.js:478 -#: htdocs/luci-static/resources/view/fchomo/client.js:491 -#: htdocs/luci-static/resources/view/fchomo/client.js:498 +#: htdocs/luci-static/resources/view/fchomo/client.js:488 +#: htdocs/luci-static/resources/view/fchomo/client.js:501 #: htdocs/luci-static/resources/view/fchomo/client.js:508 -#: htdocs/luci-static/resources/view/fchomo/client.js:515 -#: htdocs/luci-static/resources/view/fchomo/client.js:523 -#: htdocs/luci-static/resources/view/fchomo/client.js:530 -#: htdocs/luci-static/resources/view/fchomo/client.js:597 +#: htdocs/luci-static/resources/view/fchomo/client.js:518 +#: htdocs/luci-static/resources/view/fchomo/client.js:525 +#: htdocs/luci-static/resources/view/fchomo/client.js:533 +#: htdocs/luci-static/resources/view/fchomo/client.js:540 +#: htdocs/luci-static/resources/view/fchomo/client.js:607 msgid "Factor" msgstr "条件" -#: htdocs/luci-static/resources/fchomo.js:1153 +#: htdocs/luci-static/resources/fchomo.js:1203 msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" msgstr "无法执行 \"/etc/init.d/fchomo %s %s\" 原因: %s" -#: htdocs/luci-static/resources/fchomo.js:1112 +#: htdocs/luci-static/resources/fchomo.js:1162 msgid "Failed to generate %s, error: %s." msgstr "生成 %s 失败,错误:%s。" -#: htdocs/luci-static/resources/fchomo.js:1502 +#: htdocs/luci-static/resources/fchomo.js:1552 msgid "Failed to upload %s, error: %s." msgstr "上传 %s 失败,错误:%s。" -#: htdocs/luci-static/resources/fchomo.js:1521 +#: htdocs/luci-static/resources/fchomo.js:1571 msgid "Failed to upload, error: %s." msgstr "上传失败,错误:%s。" @@ -938,42 +954,42 @@ msgstr "上传失败,错误:%s。" msgid "Fallback" msgstr "自动回退" -#: htdocs/luci-static/resources/view/fchomo/client.js:1274 -#: htdocs/luci-static/resources/view/fchomo/client.js:1287 +#: htdocs/luci-static/resources/view/fchomo/client.js:1272 +#: htdocs/luci-static/resources/view/fchomo/client.js:1285 msgid "Fallback DNS server" msgstr "後備 DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1654 +#: htdocs/luci-static/resources/view/fchomo/client.js:1644 msgid "Fallback filter" msgstr "後備过滤器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1038 -#: htdocs/luci-static/resources/view/fchomo/node.js:1533 +#: htdocs/luci-static/resources/view/fchomo/client.js:1044 +#: htdocs/luci-static/resources/view/fchomo/node.js:1550 msgid "Filter nodes that meet keywords or regexps." msgstr "过滤匹配关键字或表达式的节点。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1273 +#: htdocs/luci-static/resources/view/fchomo/client.js:1271 msgid "Final DNS server" msgstr "兜底 DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1275 +#: htdocs/luci-static/resources/view/fchomo/client.js:1273 msgid "Final DNS server (For non-poisoned domains)" msgstr "兜底 DNS 服务器 (用于未被投毒污染的域名)" -#: htdocs/luci-static/resources/view/fchomo/client.js:1277 +#: htdocs/luci-static/resources/view/fchomo/client.js:1275 msgid "Final DNS server (For poisoned domains)" msgstr "兜底 DNS 服务器 (用于已被投毒污染的域名)" -#: htdocs/luci-static/resources/view/fchomo/server.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:188 msgid "Firewall" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:551 -#: htdocs/luci-static/resources/view/fchomo/server.js:494 +#: htdocs/luci-static/resources/view/fchomo/node.js:572 +#: htdocs/luci-static/resources/view/fchomo/server.js:480 msgid "Flow" msgstr "流控" -#: htdocs/luci-static/resources/view/fchomo/client.js:1027 +#: htdocs/luci-static/resources/view/fchomo/client.js:1033 msgid "" "For details, see %s." @@ -981,9 +997,9 @@ msgstr "" "实现细节请参阅 %s." -#: htdocs/luci-static/resources/view/fchomo/client.js:1004 -#: htdocs/luci-static/resources/view/fchomo/node.js:1402 -#: htdocs/luci-static/resources/view/fchomo/node.js:1525 +#: htdocs/luci-static/resources/view/fchomo/client.js:1010 +#: htdocs/luci-static/resources/view/fchomo/node.js:1419 +#: htdocs/luci-static/resources/view/fchomo/node.js:1542 msgid "" "For format see %s." @@ -991,7 +1007,7 @@ msgstr "" "格式请参阅 %s." -#: htdocs/luci-static/resources/view/fchomo/node.js:649 +#: htdocs/luci-static/resources/view/fchomo/node.js:670 msgid "Force DNS remote resolution." msgstr "强制 DNS 远程解析。" @@ -999,7 +1015,7 @@ msgstr "强制 DNS 远程解析。" msgid "Forced sniffing domain" msgstr "强制嗅探域名" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:283 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:281 msgid "Format" msgstr "格式" @@ -1010,7 +1026,7 @@ msgstr "格式" msgid "FullCombo Shark!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1006 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 msgid "GET" msgstr "" @@ -1022,10 +1038,10 @@ msgstr "GFW 域名列表版本" msgid "General" msgstr "常规" -#: htdocs/luci-static/resources/view/fchomo/client.js:891 -#: htdocs/luci-static/resources/view/fchomo/node.js:220 -#: htdocs/luci-static/resources/view/fchomo/node.js:1282 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 +#: htdocs/luci-static/resources/view/fchomo/client.js:897 +#: htdocs/luci-static/resources/view/fchomo/node.js:222 +#: htdocs/luci-static/resources/view/fchomo/node.js:1299 +#: htdocs/luci-static/resources/view/fchomo/server.js:171 msgid "General fields" msgstr "常规字段" @@ -1038,9 +1054,9 @@ msgstr "常规设置" #: htdocs/luci-static/resources/fchomo.js:510 #: htdocs/luci-static/resources/fchomo.js:512 #: htdocs/luci-static/resources/view/fchomo/global.js:593 -#: htdocs/luci-static/resources/view/fchomo/server.js:376 -#: htdocs/luci-static/resources/view/fchomo/server.js:717 -#: htdocs/luci-static/resources/view/fchomo/server.js:880 +#: htdocs/luci-static/resources/view/fchomo/server.js:343 +#: htdocs/luci-static/resources/view/fchomo/server.js:703 +#: htdocs/luci-static/resources/view/fchomo/server.js:866 msgid "Generate" msgstr "生成" @@ -1056,17 +1072,17 @@ msgstr "GeoIP 版本" msgid "GeoSite version" msgstr "GeoSite 版本" -#: htdocs/luci-static/resources/view/fchomo/client.js:1664 +#: htdocs/luci-static/resources/view/fchomo/client.js:1654 msgid "Geoip code" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1661 +#: htdocs/luci-static/resources/view/fchomo/client.js:1651 msgid "Geoip enable" msgstr "Geoip 启用" -#: htdocs/luci-static/resources/view/fchomo/client.js:1606 -#: htdocs/luci-static/resources/view/fchomo/client.js:1615 -#: htdocs/luci-static/resources/view/fchomo/client.js:1673 +#: htdocs/luci-static/resources/view/fchomo/client.js:1596 +#: htdocs/luci-static/resources/view/fchomo/client.js:1605 +#: htdocs/luci-static/resources/view/fchomo/client.js:1663 msgid "Geosite" msgstr "" @@ -1087,7 +1103,7 @@ msgstr "全局认证" msgid "Global client fingerprint" msgstr "全局客户端指纹" -#: htdocs/luci-static/resources/view/fchomo/node.js:575 +#: htdocs/luci-static/resources/view/fchomo/node.js:596 msgid "Global padding" msgstr "全局填充" @@ -1103,107 +1119,107 @@ msgstr "谷歌 FCM 端口" msgid "Grant access to fchomo configuration" msgstr "授予 fchomo 访问 UCI 配置的权限" -#: htdocs/luci-static/resources/view/fchomo/client.js:916 +#: htdocs/luci-static/resources/view/fchomo/client.js:922 msgid "Group" msgstr "组" #: htdocs/luci-static/resources/fchomo.js:126 #: htdocs/luci-static/resources/fchomo.js:158 -#: htdocs/luci-static/resources/view/fchomo/node.js:672 -#: htdocs/luci-static/resources/view/fchomo/node.js:969 -#: htdocs/luci-static/resources/view/fchomo/node.js:980 -#: htdocs/luci-static/resources/view/fchomo/server.js:955 +#: htdocs/luci-static/resources/view/fchomo/node.js:693 +#: htdocs/luci-static/resources/view/fchomo/node.js:990 +#: htdocs/luci-static/resources/view/fchomo/node.js:1001 +#: htdocs/luci-static/resources/view/fchomo/server.js:941 msgid "HTTP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:265 -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/node.js:1382 +#: htdocs/luci-static/resources/view/fchomo/node.js:267 +#: htdocs/luci-static/resources/view/fchomo/node.js:1049 +#: htdocs/luci-static/resources/view/fchomo/node.js:1399 msgid "HTTP header" msgstr "HTTP header" -#: htdocs/luci-static/resources/view/fchomo/node.js:405 +#: htdocs/luci-static/resources/view/fchomo/node.js:419 msgid "HTTP mask" msgstr "HTTP 掩码" -#: htdocs/luci-static/resources/view/fchomo/node.js:1005 +#: htdocs/luci-static/resources/view/fchomo/node.js:1026 msgid "HTTP request method" msgstr "HTTP 请求方法" -#: htdocs/luci-static/resources/view/fchomo/client.js:1439 +#: htdocs/luci-static/resources/view/fchomo/client.js:1433 msgid "HTTP/3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:296 +#: htdocs/luci-static/resources/view/fchomo/server.js:263 msgid "" "HTTP3 server behavior when authentication fails.
A 404 page will be " "returned if empty." msgstr "身份验证失败时的 HTTP3 服务器响应。默认返回 404 页面。" -#: htdocs/luci-static/resources/view/fchomo/node.js:970 -#: htdocs/luci-static/resources/view/fchomo/node.js:981 -#: htdocs/luci-static/resources/view/fchomo/server.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:991 +#: htdocs/luci-static/resources/view/fchomo/node.js:1002 +#: htdocs/luci-static/resources/view/fchomo/server.js:942 msgid "HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:846 +#: htdocs/luci-static/resources/view/fchomo/global.js:861 msgid "Handle domain" msgstr "处理域名" -#: htdocs/luci-static/resources/view/fchomo/node.js:363 +#: htdocs/luci-static/resources/view/fchomo/node.js:365 msgid "Handshake mode" msgstr "握手模式" -#: htdocs/luci-static/resources/view/fchomo/server.js:517 +#: htdocs/luci-static/resources/view/fchomo/server.js:503 msgid "Handshake target that supports TLS 1.3" msgstr "握手目标 (支援 TLS 1.3)" -#: htdocs/luci-static/resources/view/fchomo/server.js:417 +#: htdocs/luci-static/resources/view/fchomo/server.js:396 msgid "Handshake timeout" msgstr "握手超时" -#: htdocs/luci-static/resources/view/fchomo/client.js:973 -#: htdocs/luci-static/resources/view/fchomo/node.js:1493 +#: htdocs/luci-static/resources/view/fchomo/client.js:979 +#: htdocs/luci-static/resources/view/fchomo/node.js:1510 msgid "Health check URL" msgstr "健康检查 URL" -#: htdocs/luci-static/resources/view/fchomo/client.js:1002 -#: htdocs/luci-static/resources/view/fchomo/node.js:1523 +#: htdocs/luci-static/resources/view/fchomo/client.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1540 msgid "Health check expected status" msgstr "健康检查预期状态" -#: htdocs/luci-static/resources/view/fchomo/client.js:982 -#: htdocs/luci-static/resources/view/fchomo/node.js:1503 +#: htdocs/luci-static/resources/view/fchomo/client.js:988 +#: htdocs/luci-static/resources/view/fchomo/node.js:1520 msgid "Health check interval" msgstr "健康检查间隔" -#: htdocs/luci-static/resources/view/fchomo/client.js:989 -#: htdocs/luci-static/resources/view/fchomo/node.js:1510 +#: htdocs/luci-static/resources/view/fchomo/client.js:995 +#: htdocs/luci-static/resources/view/fchomo/node.js:1527 msgid "Health check timeout" msgstr "健康检查超时" -#: htdocs/luci-static/resources/view/fchomo/client.js:893 -#: htdocs/luci-static/resources/view/fchomo/node.js:1284 +#: htdocs/luci-static/resources/view/fchomo/client.js:899 +#: htdocs/luci-static/resources/view/fchomo/node.js:1301 msgid "Health fields" msgstr "健康字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:480 +#: htdocs/luci-static/resources/view/fchomo/node.js:501 msgid "Heartbeat interval" msgstr "心跳间隔" -#: htdocs/luci-static/resources/view/fchomo/client.js:1053 +#: htdocs/luci-static/resources/view/fchomo/client.js:1059 msgid "Hidden" msgstr "隐藏" -#: htdocs/luci-static/resources/view/fchomo/node.js:678 +#: htdocs/luci-static/resources/view/fchomo/node.js:699 msgid "Host that supports TLS 1.3" msgstr "主机名称 (支援 TLS 1.3)" -#: htdocs/luci-static/resources/view/fchomo/node.js:318 +#: htdocs/luci-static/resources/view/fchomo/node.js:320 msgid "Host-key" msgstr "主机密钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:313 +#: htdocs/luci-static/resources/view/fchomo/node.js:315 msgid "Host-key algorithms" msgstr "主机密钥算法" @@ -1217,21 +1233,21 @@ msgstr "" msgid "Hysteria2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1666 -#: htdocs/luci-static/resources/view/fchomo/client.js:1679 +#: htdocs/luci-static/resources/view/fchomo/client.js:1656 +#: htdocs/luci-static/resources/view/fchomo/client.js:1669 msgid "IP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1677 +#: htdocs/luci-static/resources/view/fchomo/client.js:1667 msgid "IP CIDR" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 msgid "IP override" msgstr "IP 覆写" -#: htdocs/luci-static/resources/view/fchomo/node.js:1159 -#: htdocs/luci-static/resources/view/fchomo/node.js:1479 +#: htdocs/luci-static/resources/view/fchomo/node.js:1180 +#: htdocs/luci-static/resources/view/fchomo/node.js:1496 msgid "IP version" msgstr "IP 版本" @@ -1243,110 +1259,110 @@ msgstr "仅 IPv4" msgid "IPv6 only" msgstr "仅 IPv6" -#: htdocs/luci-static/resources/view/fchomo/client.js:1256 +#: htdocs/luci-static/resources/view/fchomo/client.js:1254 #: htdocs/luci-static/resources/view/fchomo/global.js:415 msgid "IPv6 support" msgstr "IPv6 支持" -#: htdocs/luci-static/resources/view/fchomo/client.js:1059 +#: htdocs/luci-static/resources/view/fchomo/client.js:1065 msgid "Icon" msgstr "图标" -#: htdocs/luci-static/resources/view/fchomo/node.js:524 +#: htdocs/luci-static/resources/view/fchomo/node.js:545 msgid "Idle session check interval" msgstr "闲置会话检查间隔" -#: htdocs/luci-static/resources/view/fchomo/node.js:531 +#: htdocs/luci-static/resources/view/fchomo/node.js:552 msgid "Idle session timeout" msgstr "闲置会话超时" -#: htdocs/luci-static/resources/view/fchomo/server.js:445 +#: htdocs/luci-static/resources/view/fchomo/server.js:431 msgid "Idle timeout" msgstr "闲置超时" -#: htdocs/luci-static/resources/fchomo.js:1254 +#: htdocs/luci-static/resources/fchomo.js:1304 msgid "If All ports is selected, uncheck others" msgstr "如果选择了“所有端口”,则取消选中“其他”" -#: htdocs/luci-static/resources/view/fchomo/client.js:66 +#: htdocs/luci-static/resources/view/fchomo/client.js:68 msgid "If Block is selected, uncheck others" msgstr "如果选择了“阻止”,则取消选中“其他”" -#: htdocs/luci-static/resources/view/fchomo/server.js:275 +#: htdocs/luci-static/resources/view/fchomo/server.js:242 msgid "Ignore client bandwidth" msgstr "忽略客户端带宽" -#: htdocs/luci-static/resources/fchomo.js:619 +#: htdocs/luci-static/resources/fchomo.js:679 msgid "Import" msgstr "导入" -#: htdocs/luci-static/resources/view/fchomo/client.js:826 -#: htdocs/luci-static/resources/view/fchomo/client.js:885 -#: htdocs/luci-static/resources/view/fchomo/client.js:1082 -#: htdocs/luci-static/resources/view/fchomo/client.js:1124 -#: htdocs/luci-static/resources/view/fchomo/client.js:1192 -#: htdocs/luci-static/resources/view/fchomo/client.js:1220 -#: htdocs/luci-static/resources/view/fchomo/client.js:1321 -#: htdocs/luci-static/resources/view/fchomo/client.js:1361 -#: htdocs/luci-static/resources/view/fchomo/client.js:1558 -#: htdocs/luci-static/resources/view/fchomo/client.js:1583 -#: htdocs/luci-static/resources/view/fchomo/node.js:1184 -#: htdocs/luci-static/resources/view/fchomo/node.js:1270 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:140 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:228 +#: htdocs/luci-static/resources/view/fchomo/client.js:836 +#: htdocs/luci-static/resources/view/fchomo/client.js:891 +#: htdocs/luci-static/resources/view/fchomo/client.js:1088 +#: htdocs/luci-static/resources/view/fchomo/client.js:1126 +#: htdocs/luci-static/resources/view/fchomo/client.js:1194 +#: htdocs/luci-static/resources/view/fchomo/client.js:1218 +#: htdocs/luci-static/resources/view/fchomo/client.js:1319 +#: htdocs/luci-static/resources/view/fchomo/client.js:1355 +#: htdocs/luci-static/resources/view/fchomo/client.js:1552 +#: htdocs/luci-static/resources/view/fchomo/client.js:1573 +#: htdocs/luci-static/resources/view/fchomo/node.js:1205 +#: htdocs/luci-static/resources/view/fchomo/node.js:1287 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:142 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:226 msgid "Import mihomo config" msgstr "导入 mihomo 配置" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:181 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:179 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:230 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:234 msgid "Import rule-set links" msgstr "导入规则集链接" -#: htdocs/luci-static/resources/view/fchomo/node.js:278 -#: htdocs/luci-static/resources/view/fchomo/node.js:284 -#: htdocs/luci-static/resources/view/fchomo/node.js:1440 -#: htdocs/luci-static/resources/view/fchomo/node.js:1446 -#: htdocs/luci-static/resources/view/fchomo/server.js:264 -#: htdocs/luci-static/resources/view/fchomo/server.js:270 +#: htdocs/luci-static/resources/view/fchomo/node.js:280 +#: htdocs/luci-static/resources/view/fchomo/node.js:286 +#: htdocs/luci-static/resources/view/fchomo/node.js:1457 +#: htdocs/luci-static/resources/view/fchomo/node.js:1463 +#: htdocs/luci-static/resources/view/fchomo/server.js:231 +#: htdocs/luci-static/resources/view/fchomo/server.js:237 msgid "In Mbps." msgstr "单位为 Mbps。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1360 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:364 +#: htdocs/luci-static/resources/view/fchomo/node.js:1377 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:362 msgid "In bytes. %s will be used if empty." msgstr "单位为字节。留空则使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:481 -#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:509 msgid "In millisecond." msgstr "单位为毫秒。" -#: htdocs/luci-static/resources/view/fchomo/client.js:990 -#: htdocs/luci-static/resources/view/fchomo/client.js:1019 -#: htdocs/luci-static/resources/view/fchomo/node.js:1511 +#: htdocs/luci-static/resources/view/fchomo/client.js:996 +#: htdocs/luci-static/resources/view/fchomo/client.js:1025 +#: htdocs/luci-static/resources/view/fchomo/node.js:1528 msgid "In millisecond. %s will be used if empty." msgstr "单位为毫秒。留空则使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:525 -#: htdocs/luci-static/resources/view/fchomo/node.js:532 -#: htdocs/luci-static/resources/view/fchomo/server.js:446 -#: htdocs/luci-static/resources/view/fchomo/server.js:453 +#: htdocs/luci-static/resources/view/fchomo/node.js:546 +#: htdocs/luci-static/resources/view/fchomo/node.js:553 +#: htdocs/luci-static/resources/view/fchomo/server.js:432 +#: htdocs/luci-static/resources/view/fchomo/server.js:439 msgid "In seconds." msgstr "单位为秒。" -#: htdocs/luci-static/resources/view/fchomo/client.js:983 +#: htdocs/luci-static/resources/view/fchomo/client.js:989 #: htdocs/luci-static/resources/view/fchomo/global.js:425 #: htdocs/luci-static/resources/view/fchomo/global.js:430 #: htdocs/luci-static/resources/view/fchomo/global.js:515 -#: htdocs/luci-static/resources/view/fchomo/node.js:1366 -#: htdocs/luci-static/resources/view/fchomo/node.js:1504 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:370 +#: htdocs/luci-static/resources/view/fchomo/node.js:1383 +#: htdocs/luci-static/resources/view/fchomo/node.js:1521 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:368 msgid "In seconds. %s will be used if empty." msgstr "单位为秒。留空则使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:779 -#: htdocs/luci-static/resources/view/fchomo/server.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:800 +#: htdocs/luci-static/resources/view/fchomo/server.js:619 msgid "" "In the order of one Padding-Length and one Padding-" "Interval, infinite concatenation." @@ -1358,27 +1374,27 @@ msgstr "" msgid "Inbound" msgstr "入站" -#: htdocs/luci-static/resources/view/fchomo/client.js:951 +#: htdocs/luci-static/resources/view/fchomo/client.js:957 msgid "Include all" msgstr "引入所有" -#: htdocs/luci-static/resources/view/fchomo/client.js:956 +#: htdocs/luci-static/resources/view/fchomo/client.js:962 msgid "Include all node" msgstr "引入所有节点" -#: htdocs/luci-static/resources/view/fchomo/client.js:961 +#: htdocs/luci-static/resources/view/fchomo/client.js:967 msgid "Include all provider" msgstr "引入所有供应商" -#: htdocs/luci-static/resources/view/fchomo/client.js:962 +#: htdocs/luci-static/resources/view/fchomo/client.js:968 msgid "Includes all Provider." msgstr "引入所有供应商。" -#: htdocs/luci-static/resources/view/fchomo/client.js:952 +#: htdocs/luci-static/resources/view/fchomo/client.js:958 msgid "Includes all Proxy Node and Provider." msgstr "引入所有代理节点及供应商。" -#: htdocs/luci-static/resources/view/fchomo/client.js:957 +#: htdocs/luci-static/resources/view/fchomo/client.js:963 msgid "Includes all Proxy Node." msgstr "引入所有代理节点。" @@ -1386,8 +1402,8 @@ msgstr "引入所有代理节点。" msgid "Info" msgstr "信息" -#: htdocs/luci-static/resources/view/fchomo/node.js:1299 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:266 +#: htdocs/luci-static/resources/view/fchomo/node.js:1316 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 msgid "Inline" msgstr "内嵌" @@ -1399,39 +1415,39 @@ msgstr "接口控制" msgid "Keep default" msgstr "保持缺省" -#: htdocs/luci-static/resources/view/fchomo/node.js:371 -#: htdocs/luci-static/resources/view/fchomo/server.js:332 +#: htdocs/luci-static/resources/view/fchomo/node.js:373 +#: htdocs/luci-static/resources/view/fchomo/server.js:299 msgid "Key" msgstr "密钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:904 -#: htdocs/luci-static/resources/view/fchomo/server.js:807 +#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/server.js:793 msgid "Key path" msgstr "证书路径" -#: htdocs/luci-static/resources/view/fchomo/server.js:652 +#: htdocs/luci-static/resources/view/fchomo/server.js:638 msgid "Keypairs" msgstr "密钥对" -#: htdocs/luci-static/resources/view/fchomo/client.js:896 -#: htdocs/luci-static/resources/view/fchomo/client.js:1130 -#: htdocs/luci-static/resources/view/fchomo/client.js:1226 -#: htdocs/luci-static/resources/view/fchomo/client.js:1367 -#: htdocs/luci-static/resources/view/fchomo/client.js:1589 -#: htdocs/luci-static/resources/view/fchomo/node.js:227 -#: htdocs/luci-static/resources/view/fchomo/node.js:1287 -#: htdocs/luci-static/resources/view/fchomo/node.js:1572 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 -#: htdocs/luci-static/resources/view/fchomo/server.js:212 +#: htdocs/luci-static/resources/view/fchomo/client.js:902 +#: htdocs/luci-static/resources/view/fchomo/client.js:1132 +#: htdocs/luci-static/resources/view/fchomo/client.js:1224 +#: htdocs/luci-static/resources/view/fchomo/client.js:1361 +#: htdocs/luci-static/resources/view/fchomo/client.js:1579 +#: htdocs/luci-static/resources/view/fchomo/node.js:229 +#: htdocs/luci-static/resources/view/fchomo/node.js:1304 +#: htdocs/luci-static/resources/view/fchomo/node.js:1589 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:244 +#: htdocs/luci-static/resources/view/fchomo/server.js:179 msgid "Label" msgstr "标签" -#: htdocs/luci-static/resources/view/fchomo/client.js:996 -#: htdocs/luci-static/resources/view/fchomo/node.js:1517 +#: htdocs/luci-static/resources/view/fchomo/client.js:1002 +#: htdocs/luci-static/resources/view/fchomo/node.js:1534 msgid "Lazy" msgstr "懒惰状态" -#: htdocs/luci-static/resources/view/fchomo/server.js:503 +#: htdocs/luci-static/resources/view/fchomo/server.js:489 msgid "" "Legacy protocol support (VMess MD5 Authentication) is provided for " "compatibility purposes only, use of alterId > 1 is not recommended." @@ -1443,16 +1459,16 @@ msgstr "" msgid "Less compatibility and sometimes better performance." msgstr "有时性能更好。" -#: htdocs/luci-static/resources/view/fchomo/node.js:831 -#: htdocs/luci-static/resources/view/fchomo/server.js:788 +#: htdocs/luci-static/resources/view/fchomo/node.js:852 +#: htdocs/luci-static/resources/view/fchomo/server.js:774 msgid "List of supported application level protocols, in order of preference." msgstr "支持的应用层协议协商列表,按顺序排列。" -#: htdocs/luci-static/resources/view/fchomo/server.js:232 +#: htdocs/luci-static/resources/view/fchomo/server.js:199 msgid "Listen address" msgstr "监听地址" -#: htdocs/luci-static/resources/view/fchomo/server.js:209 +#: htdocs/luci-static/resources/view/fchomo/server.js:176 msgid "Listen fields" msgstr "监听字段" @@ -1460,8 +1476,8 @@ msgstr "监听字段" msgid "Listen interfaces" msgstr "监听接口" -#: htdocs/luci-static/resources/view/fchomo/client.js:1251 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/client.js:1249 +#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Listen port" msgstr "监听端口" @@ -1473,16 +1489,16 @@ msgstr "监听端口" msgid "Load balance" msgstr "负载均衡" -#: htdocs/luci-static/resources/view/fchomo/node.js:1297 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 +#: htdocs/luci-static/resources/view/fchomo/node.js:1314 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:262 msgid "Local" msgstr "本地" -#: htdocs/luci-static/resources/view/fchomo/node.js:602 +#: htdocs/luci-static/resources/view/fchomo/node.js:623 msgid "Local IPv6 address" msgstr "本地 IPv6 地址" -#: htdocs/luci-static/resources/view/fchomo/node.js:595 +#: htdocs/luci-static/resources/view/fchomo/node.js:616 msgid "Local address" msgstr "本地地址" @@ -1507,75 +1523,75 @@ msgid "Lowercase only" msgstr "仅限小写" #: htdocs/luci-static/resources/view/fchomo/global.js:502 -#: htdocs/luci-static/resources/view/fchomo/node.js:642 +#: htdocs/luci-static/resources/view/fchomo/node.js:663 msgid "MTU" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:295 +#: htdocs/luci-static/resources/view/fchomo/server.js:262 msgid "Masquerade" msgstr "伪装" -#: htdocs/luci-static/resources/view/fchomo/client.js:1611 +#: htdocs/luci-static/resources/view/fchomo/client.js:1601 msgid "Match domain. Support wildcards." msgstr "匹配域名。支持通配符。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1683 +#: htdocs/luci-static/resources/view/fchomo/client.js:1673 msgid "Match domain. Support wildcards.
" msgstr "匹配域名。支持通配符。
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1616 +#: htdocs/luci-static/resources/view/fchomo/client.js:1606 msgid "Match geosite." msgstr "匹配 geosite。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1674 +#: htdocs/luci-static/resources/view/fchomo/client.js:1664 msgid "Match geosite.
" msgstr "匹配 geosite。
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1655 msgid "Match response with geoip.
" msgstr "匹配响应通过 geoip。
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1678 +#: htdocs/luci-static/resources/view/fchomo/client.js:1668 msgid "Match response with ipcidr.
" msgstr "匹配响应通过 ipcidr
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1621 +#: htdocs/luci-static/resources/view/fchomo/client.js:1611 msgid "Match rule set." msgstr "匹配规则集。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1038 +#: htdocs/luci-static/resources/view/fchomo/node.js:1059 msgid "Max Early Data" msgstr "前置数据最大值" -#: htdocs/luci-static/resources/view/fchomo/node.js:467 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/server.js:425 msgid "Max UDP relay packet size" msgstr "UDP 中继数据包最大尺寸" -#: htdocs/luci-static/resources/view/fchomo/client.js:1010 +#: htdocs/luci-static/resources/view/fchomo/client.js:1016 msgid "Max count of failures" msgstr "最大失败次数" -#: htdocs/luci-static/resources/view/fchomo/node.js:283 -#: htdocs/luci-static/resources/view/fchomo/server.js:269 +#: htdocs/luci-static/resources/view/fchomo/node.js:285 +#: htdocs/luci-static/resources/view/fchomo/server.js:236 msgid "Max download speed" msgstr "最大下载速度" -#: htdocs/luci-static/resources/view/fchomo/node.js:494 +#: htdocs/luci-static/resources/view/fchomo/node.js:515 msgid "Max open streams" msgstr "限制打开流的数量" -#: htdocs/luci-static/resources/view/fchomo/node.js:277 -#: htdocs/luci-static/resources/view/fchomo/server.js:263 +#: htdocs/luci-static/resources/view/fchomo/node.js:279 +#: htdocs/luci-static/resources/view/fchomo/server.js:230 msgid "Max upload speed" msgstr "最大上传速度" -#: htdocs/luci-static/resources/view/fchomo/node.js:1075 -#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/node.js:1096 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "Maximum connections" msgstr "最大连接数" -#: htdocs/luci-static/resources/view/fchomo/node.js:1089 +#: htdocs/luci-static/resources/view/fchomo/node.js:1110 msgid "" "Maximum multiplexed streams in a connection before opening a new connection." "
Conflict with %s and %s." @@ -1583,12 +1599,12 @@ msgstr "" "在打开新连接之前,连接中的最大多路复用流数量。
%s 和 " "%s 冲突。" -#: htdocs/luci-static/resources/view/fchomo/node.js:398 -#: htdocs/luci-static/resources/view/fchomo/server.js:410 +#: htdocs/luci-static/resources/view/fchomo/node.js:412 +#: htdocs/luci-static/resources/view/fchomo/server.js:389 msgid "Maximum padding" msgstr "最大填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1088 +#: htdocs/luci-static/resources/view/fchomo/node.js:1109 msgid "Maximum streams" msgstr "最大流数量" @@ -1597,7 +1613,7 @@ msgstr "最大流数量" msgid "Mieru" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:781 +#: htdocs/luci-static/resources/view/fchomo/client.js:791 #: htdocs/luci-static/resources/view/fchomo/log.js:149 #: htdocs/luci-static/resources/view/fchomo/log.js:154 msgid "Mihomo client" @@ -1605,26 +1621,26 @@ msgstr "Mihomo 客户端" #: htdocs/luci-static/resources/view/fchomo/log.js:158 #: htdocs/luci-static/resources/view/fchomo/log.js:163 -#: htdocs/luci-static/resources/view/fchomo/server.js:164 +#: htdocs/luci-static/resources/view/fchomo/server.js:131 msgid "Mihomo server" msgstr "Mihomo 服务端" -#: htdocs/luci-static/resources/view/fchomo/node.js:538 +#: htdocs/luci-static/resources/view/fchomo/node.js:559 msgid "Min of idle sessions to keep" msgstr "要保留的最少闲置会话数" -#: htdocs/luci-static/resources/view/fchomo/node.js:1082 +#: htdocs/luci-static/resources/view/fchomo/node.js:1103 msgid "" "Minimum multiplexed streams in a connection before opening a new connection." msgstr "在打开新连接之前,连接中的最小多路复用流数量。" -#: htdocs/luci-static/resources/view/fchomo/node.js:391 -#: htdocs/luci-static/resources/view/fchomo/server.js:403 +#: htdocs/luci-static/resources/view/fchomo/node.js:405 +#: htdocs/luci-static/resources/view/fchomo/server.js:382 msgid "Minimum padding" msgstr "最小填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1081 -#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/node.js:1102 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "Minimum streams" msgstr "最小流数量" @@ -1641,33 +1657,33 @@ msgstr "混合 系统 TCP 栈和 gVisor UDP 栈。" msgid "Mixed port" msgstr "混合端口" -#: htdocs/luci-static/resources/view/fchomo/node.js:1061 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 msgid "Multiplex" msgstr "多路复用" -#: htdocs/luci-static/resources/view/fchomo/node.js:224 -#: htdocs/luci-static/resources/view/fchomo/server.js:208 +#: htdocs/luci-static/resources/view/fchomo/node.js:226 +#: htdocs/luci-static/resources/view/fchomo/server.js:175 msgid "Multiplex fields" msgstr "多路复用字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:354 +#: htdocs/luci-static/resources/view/fchomo/node.js:356 msgid "Multiplexing" msgstr "多路复用" -#: htdocs/luci-static/resources/view/fchomo/client.js:541 -#: htdocs/luci-static/resources/view/fchomo/client.js:616 +#: htdocs/luci-static/resources/view/fchomo/client.js:551 +#: htdocs/luci-static/resources/view/fchomo/client.js:626 msgid "NOT" msgstr "NOT" -#: htdocs/luci-static/resources/view/fchomo/node.js:1372 +#: htdocs/luci-static/resources/view/fchomo/node.js:1389 msgid "Name of the Proxy group to download provider." msgstr "用于下载供应商订阅的代理组名称。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:376 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:374 msgid "Name of the Proxy group to download rule set." msgstr "用于下载规则集订阅的代理组名称。" -#: htdocs/luci-static/resources/view/fchomo/node.js:451 +#: htdocs/luci-static/resources/view/fchomo/node.js:472 msgid "Native UDP" msgstr "原生 UDP" @@ -1679,45 +1695,45 @@ msgstr "原生外观" msgid "No Authentication IP ranges" msgstr "无需认证的 IP 范围" -#: htdocs/luci-static/resources/view/fchomo/client.js:1387 +#: htdocs/luci-static/resources/view/fchomo/client.js:1381 msgid "No add'l params" msgstr "无附加参数" -#: htdocs/luci-static/resources/view/fchomo/client.js:997 -#: htdocs/luci-static/resources/view/fchomo/node.js:1518 +#: htdocs/luci-static/resources/view/fchomo/client.js:1003 +#: htdocs/luci-static/resources/view/fchomo/node.js:1535 msgid "No testing is performed when this provider node is not in use." msgstr "当此供应商的节点未使用时,不执行任何测试。" -#: htdocs/luci-static/resources/fchomo.js:570 +#: htdocs/luci-static/resources/fchomo.js:640 msgid "No valid %s found." msgstr "未找到有效的%s。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:207 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:205 msgid "No valid rule-set link found." msgstr "未找到有效的规则集链接。" -#: htdocs/luci-static/resources/view/fchomo/client.js:923 -#: htdocs/luci-static/resources/view/fchomo/node.js:215 +#: htdocs/luci-static/resources/view/fchomo/client.js:929 +#: htdocs/luci-static/resources/view/fchomo/node.js:217 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 msgid "Node" msgstr "节点" -#: htdocs/luci-static/resources/view/fchomo/client.js:1042 -#: htdocs/luci-static/resources/view/fchomo/node.js:1538 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +#: htdocs/luci-static/resources/view/fchomo/node.js:1555 msgid "Node exclude filter" msgstr "排除节点" -#: htdocs/luci-static/resources/view/fchomo/client.js:1047 -#: htdocs/luci-static/resources/view/fchomo/node.js:1545 +#: htdocs/luci-static/resources/view/fchomo/client.js:1053 +#: htdocs/luci-static/resources/view/fchomo/node.js:1562 msgid "Node exclude type" msgstr "排除节点类型" -#: htdocs/luci-static/resources/view/fchomo/client.js:1037 -#: htdocs/luci-static/resources/view/fchomo/node.js:1532 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +#: htdocs/luci-static/resources/view/fchomo/node.js:1549 msgid "Node filter" msgstr "过滤节点" -#: htdocs/luci-static/resources/view/fchomo/client.js:1018 +#: htdocs/luci-static/resources/view/fchomo/client.js:1024 msgid "Node switch tolerance" msgstr "节点切换容差" @@ -1729,37 +1745,37 @@ msgstr "无" msgid "Not Installed" msgstr "未安装" -#: htdocs/luci-static/resources/fchomo.js:1038 +#: htdocs/luci-static/resources/fchomo.js:1088 msgid "Not Running" msgstr "未在运行" -#: htdocs/luci-static/resources/view/fchomo/node.js:671 +#: htdocs/luci-static/resources/view/fchomo/node.js:692 msgid "Obfs Mode" msgstr "Obfs 模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:295 -#: htdocs/luci-static/resources/view/fchomo/server.js:287 +#: htdocs/luci-static/resources/view/fchomo/node.js:297 +#: htdocs/luci-static/resources/view/fchomo/server.js:254 msgid "Obfuscate password" msgstr "混淆密码" -#: htdocs/luci-static/resources/view/fchomo/node.js:289 -#: htdocs/luci-static/resources/view/fchomo/node.js:385 -#: htdocs/luci-static/resources/view/fchomo/server.js:281 -#: htdocs/luci-static/resources/view/fchomo/server.js:397 +#: htdocs/luci-static/resources/view/fchomo/node.js:291 +#: htdocs/luci-static/resources/view/fchomo/node.js:395 +#: htdocs/luci-static/resources/view/fchomo/server.js:248 +#: htdocs/luci-static/resources/view/fchomo/server.js:372 msgid "Obfuscate type" msgstr "混淆类型" -#: htdocs/luci-static/resources/view/fchomo/node.js:386 -#: htdocs/luci-static/resources/view/fchomo/server.js:398 +#: htdocs/luci-static/resources/view/fchomo/node.js:396 +#: htdocs/luci-static/resources/view/fchomo/server.js:373 msgid "Obfuscated as ASCII data stream" msgstr "混淆为 ASCII 数据流" -#: htdocs/luci-static/resources/view/fchomo/node.js:387 -#: htdocs/luci-static/resources/view/fchomo/server.js:399 +#: htdocs/luci-static/resources/view/fchomo/node.js:397 +#: htdocs/luci-static/resources/view/fchomo/server.js:374 msgid "Obfuscated as low-entropy data stream" msgstr "混淆为低熵数据流" -#: htdocs/luci-static/resources/view/fchomo/global.js:871 +#: htdocs/luci-static/resources/view/fchomo/global.js:886 msgid "One or more numbers in the range 0-63 separated by commas" msgstr "0-63 范围内的一个或多个数字,以逗号分隔" @@ -1767,7 +1783,7 @@ msgstr "0-63 范围内的一个或多个数字,以逗号分隔" msgid "Only process traffic from specific interfaces. Leave empty for all." msgstr "只处理来自指定接口的流量。留空表示全部。" -#: htdocs/luci-static/resources/fchomo.js:1032 +#: htdocs/luci-static/resources/fchomo.js:1082 msgid "Open Dashboard" msgstr "打开面板" @@ -1780,16 +1796,16 @@ msgstr "运行模式" msgid "Override destination" msgstr "覆盖目标地址" -#: htdocs/luci-static/resources/view/fchomo/client.js:892 -#: htdocs/luci-static/resources/view/fchomo/node.js:1283 +#: htdocs/luci-static/resources/view/fchomo/client.js:898 +#: htdocs/luci-static/resources/view/fchomo/node.js:1300 msgid "Override fields" msgstr "覆盖字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:434 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 msgid "Override the IP address of the server that DNS response." msgstr "覆盖 DNS 回应的服务器的 IP 地址。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1644 +#: htdocs/luci-static/resources/view/fchomo/client.js:1634 msgid "Override the Proxy group of DNS server." msgstr "覆盖 DNS 服务器所使用的代理组。" @@ -1797,7 +1813,7 @@ msgstr "覆盖 DNS 服务器所使用的代理组。" msgid "Override the connection destination address with the sniffed domain." msgstr "使用嗅探到的域名覆盖连接目标。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1491 +#: htdocs/luci-static/resources/view/fchomo/client.js:1485 msgid "Override the existing ECS in original request." msgstr "覆盖原始请求中已有的 ECS。" @@ -1805,43 +1821,43 @@ msgstr "覆盖原始请求中已有的 ECS。" msgid "Overview" msgstr "概览" -#: htdocs/luci-static/resources/view/fchomo/node.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:1028 msgid "POST" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1029 msgid "PUT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:587 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 msgid "Packet encoding" msgstr "数据包编码" -#: htdocs/luci-static/resources/view/fchomo/server.js:483 +#: htdocs/luci-static/resources/view/fchomo/server.js:469 msgid "Padding scheme" msgstr "填充方案" -#: htdocs/luci-static/resources/view/fchomo/node.js:777 -#: htdocs/luci-static/resources/view/fchomo/server.js:631 +#: htdocs/luci-static/resources/view/fchomo/node.js:798 +#: htdocs/luci-static/resources/view/fchomo/server.js:617 msgid "Paddings" msgstr "填充 (Paddings)" -#: htdocs/luci-static/resources/view/fchomo/node.js:259 -#: htdocs/luci-static/resources/view/fchomo/node.js:332 -#: htdocs/luci-static/resources/view/fchomo/node.js:686 -#: htdocs/luci-static/resources/view/fchomo/server.js:254 -#: htdocs/luci-static/resources/view/fchomo/server.js:310 -#: htdocs/luci-static/resources/view/fchomo/server.js:524 +#: htdocs/luci-static/resources/view/fchomo/node.js:261 +#: htdocs/luci-static/resources/view/fchomo/node.js:334 +#: htdocs/luci-static/resources/view/fchomo/node.js:707 +#: htdocs/luci-static/resources/view/fchomo/server.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:277 +#: htdocs/luci-static/resources/view/fchomo/server.js:510 msgid "Password" msgstr "密码" -#: htdocs/luci-static/resources/view/fchomo/node.js:1347 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:351 -#: htdocs/luci-static/resources/view/fchomo/server.js:571 +#: htdocs/luci-static/resources/view/fchomo/node.js:1364 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:349 +#: htdocs/luci-static/resources/view/fchomo/server.js:557 msgid "Payload" msgstr "Payload" -#: htdocs/luci-static/resources/view/fchomo/node.js:616 +#: htdocs/luci-static/resources/view/fchomo/node.js:637 msgid "Peer pubkic key" msgstr "对端公钥" @@ -1851,11 +1867,11 @@ msgid "" "it is not needed." msgstr "性能可能会略有下降,建议仅在需要时开启。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:282 msgid "Plain text" msgstr "纯文本 text" -#: htdocs/luci-static/resources/view/fchomo/global.js:848 +#: htdocs/luci-static/resources/view/fchomo/global.js:863 msgid "" "Please ensure that the DNS query of the domains to be processed in the DNS " "policy
are send via DIRECT/Proxy Node in the same semantics as Routing " @@ -1864,58 +1880,58 @@ msgstr "" "请在 DNS 策略 中确保要处理的域名的 DNS 查询
以与路由模式相同的语义通过 直" "连/代理节点 发送。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:184 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:182 msgid "" "Please refer to
%s for link format " "standards." msgstr "链接格式标准请参考 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1320 -#: htdocs/luci-static/resources/view/fchomo/node.js:1346 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:324 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:350 +#: htdocs/luci-static/resources/view/fchomo/node.js:1337 +#: htdocs/luci-static/resources/view/fchomo/node.js:1363 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:322 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:348 msgid "" "Please type %s." msgstr "" "请输入 %s。" -#: htdocs/luci-static/resources/view/fchomo/client.js:827 -#: htdocs/luci-static/resources/view/fchomo/client.js:1083 -#: htdocs/luci-static/resources/view/fchomo/client.js:1193 -#: htdocs/luci-static/resources/view/fchomo/client.js:1322 -#: htdocs/luci-static/resources/view/fchomo/client.js:1559 -#: htdocs/luci-static/resources/view/fchomo/node.js:1185 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:141 +#: htdocs/luci-static/resources/view/fchomo/client.js:837 +#: htdocs/luci-static/resources/view/fchomo/client.js:1089 +#: htdocs/luci-static/resources/view/fchomo/client.js:1195 +#: htdocs/luci-static/resources/view/fchomo/client.js:1320 +#: htdocs/luci-static/resources/view/fchomo/client.js:1553 +#: htdocs/luci-static/resources/view/fchomo/node.js:1206 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:143 msgid "Please type %s fields of mihomo config.
" msgstr "请输入 mihomo 配置的 %s 字段。
" -#: htdocs/luci-static/resources/view/fchomo/node.js:660 -#: htdocs/luci-static/resources/view/fchomo/server.js:510 +#: htdocs/luci-static/resources/view/fchomo/node.js:681 +#: htdocs/luci-static/resources/view/fchomo/server.js:496 msgid "Plugin" msgstr "插件" -#: htdocs/luci-static/resources/view/fchomo/node.js:671 -#: htdocs/luci-static/resources/view/fchomo/node.js:678 -#: htdocs/luci-static/resources/view/fchomo/node.js:686 #: htdocs/luci-static/resources/view/fchomo/node.js:692 -#: htdocs/luci-static/resources/view/fchomo/node.js:700 -#: htdocs/luci-static/resources/view/fchomo/node.js:706 -#: htdocs/luci-static/resources/view/fchomo/server.js:517 -#: htdocs/luci-static/resources/view/fchomo/server.js:524 -#: htdocs/luci-static/resources/view/fchomo/server.js:530 +#: htdocs/luci-static/resources/view/fchomo/node.js:699 +#: htdocs/luci-static/resources/view/fchomo/node.js:707 +#: htdocs/luci-static/resources/view/fchomo/node.js:713 +#: htdocs/luci-static/resources/view/fchomo/node.js:721 +#: htdocs/luci-static/resources/view/fchomo/node.js:727 +#: htdocs/luci-static/resources/view/fchomo/server.js:503 +#: htdocs/luci-static/resources/view/fchomo/server.js:510 +#: htdocs/luci-static/resources/view/fchomo/server.js:516 msgid "Plugin:" msgstr "插件:" -#: htdocs/luci-static/resources/view/fchomo/node.js:247 +#: htdocs/luci-static/resources/view/fchomo/node.js:249 msgid "Port" msgstr "端口" -#: htdocs/luci-static/resources/fchomo.js:1263 +#: htdocs/luci-static/resources/fchomo.js:1313 msgid "Port %s alrealy exists!" msgstr "端口 %s 已存在!" -#: htdocs/luci-static/resources/view/fchomo/node.js:342 +#: htdocs/luci-static/resources/view/fchomo/node.js:344 msgid "Port range" msgstr "端口范围" @@ -1923,13 +1939,13 @@ msgstr "端口范围" msgid "Ports" msgstr "端口" -#: htdocs/luci-static/resources/view/fchomo/node.js:272 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/node.js:274 +#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Ports pool" msgstr "端口池" -#: htdocs/luci-static/resources/view/fchomo/node.js:411 -#: htdocs/luci-static/resources/view/fchomo/node.js:623 +#: htdocs/luci-static/resources/view/fchomo/node.js:432 +#: htdocs/luci-static/resources/view/fchomo/node.js:644 msgid "Pre-shared key" msgstr "预共享密钥" @@ -1948,22 +1964,22 @@ msgstr "防止某些情况下的 ICMP 环回问题。Ping 不会显示实际延 #: htdocs/luci-static/resources/view/fchomo/global.js:742 #: htdocs/luci-static/resources/view/fchomo/global.js:759 -#: htdocs/luci-static/resources/view/fchomo/node.js:1149 -#: htdocs/luci-static/resources/view/fchomo/node.js:1155 -#: htdocs/luci-static/resources/view/fchomo/node.js:1467 -#: htdocs/luci-static/resources/view/fchomo/node.js:1474 +#: htdocs/luci-static/resources/view/fchomo/node.js:1170 +#: htdocs/luci-static/resources/view/fchomo/node.js:1176 +#: htdocs/luci-static/resources/view/fchomo/node.js:1484 +#: htdocs/luci-static/resources/view/fchomo/node.js:1491 msgid "Priority: Proxy Node > Global." msgstr "优先级: 代理节点 > 全局。" -#: htdocs/luci-static/resources/view/fchomo/node.js:304 +#: htdocs/luci-static/resources/view/fchomo/node.js:306 msgid "Priv-key" msgstr "密钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:308 +#: htdocs/luci-static/resources/view/fchomo/node.js:310 msgid "Priv-key passphrase" msgstr "密钥密码" -#: htdocs/luci-static/resources/view/fchomo/node.js:608 +#: htdocs/luci-static/resources/view/fchomo/node.js:629 msgid "Private key" msgstr "私钥" @@ -1972,34 +1988,34 @@ msgid "Process matching mode" msgstr "进程匹配模式" #: htdocs/luci-static/resources/view/fchomo/global.js:690 -#: htdocs/luci-static/resources/view/fchomo/node.js:1067 +#: htdocs/luci-static/resources/view/fchomo/node.js:1088 msgid "Protocol" msgstr "协议" -#: htdocs/luci-static/resources/view/fchomo/node.js:582 +#: htdocs/luci-static/resources/view/fchomo/node.js:603 msgid "Protocol parameter. Enable length block encryption." msgstr "协议参数。启用长度块加密。" -#: htdocs/luci-static/resources/view/fchomo/node.js:576 +#: htdocs/luci-static/resources/view/fchomo/node.js:597 msgid "" "Protocol parameter. Will waste traffic randomly if enabled (enabled by " "default in v2ray and cannot be disabled)." msgstr "协议参数。 如启用会随机浪费流量(在 v2ray 中默认启用并且无法禁用)。" -#: htdocs/luci-static/resources/view/fchomo/client.js:937 -#: htdocs/luci-static/resources/view/fchomo/node.js:1168 -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 -#: htdocs/luci-static/resources/view/fchomo/node.js:1583 +#: htdocs/luci-static/resources/view/fchomo/client.js:943 +#: htdocs/luci-static/resources/view/fchomo/node.js:1189 +#: htdocs/luci-static/resources/view/fchomo/node.js:1198 +#: htdocs/luci-static/resources/view/fchomo/node.js:1600 msgid "Provider" msgstr "供应商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1353 +#: htdocs/luci-static/resources/view/fchomo/node.js:1370 msgid "Provider URL" msgstr "供应商订阅 URL" -#: htdocs/luci-static/resources/view/fchomo/client.js:801 -#: htdocs/luci-static/resources/view/fchomo/client.js:819 -#: htdocs/luci-static/resources/view/fchomo/client.js:1278 +#: htdocs/luci-static/resources/view/fchomo/client.js:811 +#: htdocs/luci-static/resources/view/fchomo/client.js:829 +#: htdocs/luci-static/resources/view/fchomo/client.js:1276 msgid "Proxy Group" msgstr "代理组" @@ -2015,24 +2031,24 @@ msgstr "代理 IPv6 地址" msgid "Proxy MAC-s" msgstr "代理 MAC 地址" -#: htdocs/luci-static/resources/view/fchomo/node.js:206 -#: htdocs/luci-static/resources/view/fchomo/node.js:1582 +#: htdocs/luci-static/resources/view/fchomo/node.js:208 +#: htdocs/luci-static/resources/view/fchomo/node.js:1599 msgid "Proxy Node" msgstr "代理节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:1558 -#: htdocs/luci-static/resources/view/fchomo/node.js:1567 +#: htdocs/luci-static/resources/view/fchomo/node.js:1575 +#: htdocs/luci-static/resources/view/fchomo/node.js:1584 msgid "Proxy chain" msgstr "代理链" -#: htdocs/luci-static/resources/view/fchomo/client.js:713 -#: htdocs/luci-static/resources/view/fchomo/client.js:1422 -#: htdocs/luci-static/resources/view/fchomo/node.js:1371 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:375 +#: htdocs/luci-static/resources/view/fchomo/client.js:723 +#: htdocs/luci-static/resources/view/fchomo/client.js:1416 +#: htdocs/luci-static/resources/view/fchomo/node.js:1388 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:373 msgid "Proxy group" msgstr "代理组" -#: htdocs/luci-static/resources/view/fchomo/client.js:1643 +#: htdocs/luci-static/resources/view/fchomo/client.js:1633 msgid "Proxy group override" msgstr "代理组覆盖" @@ -2044,54 +2060,54 @@ msgstr "代理模式" msgid "Proxy routerself" msgstr "代理路由器自身" -#: htdocs/luci-static/resources/view/fchomo/node.js:452 +#: htdocs/luci-static/resources/view/fchomo/node.js:473 msgid "QUIC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:440 -#: htdocs/luci-static/resources/view/fchomo/server.js:431 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/server.js:417 msgid "QUIC congestion controller." msgstr "QUIC 拥塞控制器。" -#: htdocs/luci-static/resources/view/fchomo/client.js:804 -#: htdocs/luci-static/resources/view/fchomo/server.js:185 +#: htdocs/luci-static/resources/view/fchomo/client.js:814 +#: htdocs/luci-static/resources/view/fchomo/server.js:152 msgid "Quick Reload" msgstr "快速重载" -#: htdocs/luci-static/resources/view/fchomo/node.js:940 -#: htdocs/luci-static/resources/view/fchomo/server.js:895 +#: htdocs/luci-static/resources/view/fchomo/node.js:961 +#: htdocs/luci-static/resources/view/fchomo/server.js:881 msgid "REALITY" msgstr "REALITY" -#: htdocs/luci-static/resources/view/fchomo/node.js:955 +#: htdocs/luci-static/resources/view/fchomo/node.js:976 msgid "REALITY X25519MLKEM768 PQC support" msgstr "REALITY X25519MLKEM768 后量子加密支持" -#: htdocs/luci-static/resources/view/fchomo/server.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:918 msgid "REALITY certificate issued to" msgstr "REALITY 证书颁发给" -#: htdocs/luci-static/resources/view/fchomo/server.js:900 +#: htdocs/luci-static/resources/view/fchomo/server.js:886 msgid "REALITY handshake server" msgstr "REALITY 握手服务器" -#: htdocs/luci-static/resources/view/fchomo/server.js:907 +#: htdocs/luci-static/resources/view/fchomo/server.js:893 msgid "REALITY private key" msgstr "REALITY 私钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:945 -#: htdocs/luci-static/resources/view/fchomo/server.js:922 +#: htdocs/luci-static/resources/view/fchomo/node.js:966 +#: htdocs/luci-static/resources/view/fchomo/server.js:908 msgid "REALITY public key" msgstr "REALITY 公钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:950 -#: htdocs/luci-static/resources/view/fchomo/server.js:926 +#: htdocs/luci-static/resources/view/fchomo/node.js:971 +#: htdocs/luci-static/resources/view/fchomo/server.js:912 msgid "REALITY short ID" msgstr "REALITY 标识符" -#: htdocs/luci-static/resources/view/fchomo/node.js:767 -#: htdocs/luci-static/resources/view/fchomo/server.js:602 -#: htdocs/luci-static/resources/view/fchomo/server.js:621 +#: htdocs/luci-static/resources/view/fchomo/node.js:788 +#: htdocs/luci-static/resources/view/fchomo/server.js:588 +#: htdocs/luci-static/resources/view/fchomo/server.js:607 msgid "RTT" msgstr "" @@ -2127,10 +2143,10 @@ msgstr "Redirect TCP + Tun UDP" msgid "Refresh every %s seconds." msgstr "每 %s 秒刷新。" -#: htdocs/luci-static/resources/fchomo.js:1025 -#: htdocs/luci-static/resources/view/fchomo/client.js:805 +#: htdocs/luci-static/resources/fchomo.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:815 #: htdocs/luci-static/resources/view/fchomo/global.js:193 -#: htdocs/luci-static/resources/view/fchomo/server.js:186 +#: htdocs/luci-static/resources/view/fchomo/server.js:153 msgid "Reload" msgstr "重载" @@ -2138,32 +2154,32 @@ msgstr "重载" msgid "Reload All" msgstr "重载所有" -#: htdocs/luci-static/resources/view/fchomo/node.js:1298 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:265 +#: htdocs/luci-static/resources/view/fchomo/node.js:1315 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 msgid "Remote" msgstr "远程" -#: htdocs/luci-static/resources/view/fchomo/node.js:648 +#: htdocs/luci-static/resources/view/fchomo/node.js:669 msgid "Remote DNS resolve" msgstr "远程 DNS 解析" -#: htdocs/luci-static/resources/fchomo.js:1184 +#: htdocs/luci-static/resources/fchomo.js:1234 msgid "Remove" msgstr "移除" -#: htdocs/luci-static/resources/fchomo.js:1189 -#: htdocs/luci-static/resources/view/fchomo/node.js:1274 -#: htdocs/luci-static/resources/view/fchomo/node.js:1276 +#: htdocs/luci-static/resources/fchomo.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1291 +#: htdocs/luci-static/resources/view/fchomo/node.js:1293 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:236 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:238 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:240 msgid "Remove idles" msgstr "移除闲置" -#: htdocs/luci-static/resources/view/fchomo/node.js:1400 +#: htdocs/luci-static/resources/view/fchomo/node.js:1417 msgid "Replace name" msgstr "名称替换" -#: htdocs/luci-static/resources/view/fchomo/node.js:1401 +#: htdocs/luci-static/resources/view/fchomo/node.js:1418 msgid "Replace node name." msgstr "替换节点名称" @@ -2171,13 +2187,13 @@ msgstr "替换节点名称" msgid "Request" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1014 -#: htdocs/luci-static/resources/view/fchomo/node.js:1021 -#: htdocs/luci-static/resources/view/fchomo/server.js:974 +#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/view/fchomo/node.js:1042 +#: htdocs/luci-static/resources/view/fchomo/server.js:960 msgid "Request path" msgstr "请求路径" -#: htdocs/luci-static/resources/view/fchomo/node.js:487 +#: htdocs/luci-static/resources/view/fchomo/node.js:508 msgid "Request timeout" msgstr "请求超时" @@ -2190,11 +2206,11 @@ msgid "Require any" msgstr "" #: htdocs/luci-static/resources/fchomo.js:347 -#: htdocs/luci-static/resources/view/fchomo/node.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:977 msgid "Requires server support." msgstr "需要服务器支持。" -#: htdocs/luci-static/resources/view/fchomo/node.js:637 +#: htdocs/luci-static/resources/view/fchomo/node.js:658 msgid "Reserved field bytes" msgstr "保留字段字节" @@ -2202,16 +2218,16 @@ msgstr "保留字段字节" msgid "Resources management" msgstr "资源管理" -#: htdocs/luci-static/resources/view/fchomo/node.js:706 +#: htdocs/luci-static/resources/view/fchomo/node.js:727 msgid "Restls script" msgstr "Restls 剧本" -#: htdocs/luci-static/resources/view/fchomo/client.js:1054 +#: htdocs/luci-static/resources/view/fchomo/client.js:1060 msgid "" "Returns hidden status in the API to hide the display of this proxy group." msgstr "在 API 返回 hidden 状态,以隐藏该代理组显示。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1060 +#: htdocs/luci-static/resources/view/fchomo/client.js:1066 msgid "" "Returns the string input for icon in the API to display in this proxy group." msgstr "在 API 返回 icon 所输入的字符串,以在该代理组显示。" @@ -2220,17 +2236,17 @@ msgstr "在 API 返回 icon 所输入的字符串,以在该代理组显示。" msgid "Routing Control" msgstr "路由控制" -#: htdocs/luci-static/resources/view/fchomo/global.js:860 -#: htdocs/luci-static/resources/view/fchomo/global.js:863 +#: htdocs/luci-static/resources/view/fchomo/global.js:875 +#: htdocs/luci-static/resources/view/fchomo/global.js:878 msgid "Routing DSCP" msgstr "路由 DSCP" -#: htdocs/luci-static/resources/view/fchomo/global.js:844 +#: htdocs/luci-static/resources/view/fchomo/global.js:845 msgid "Routing GFW" msgstr "路由 GFW 流量" -#: htdocs/luci-static/resources/view/fchomo/node.js:1154 -#: htdocs/luci-static/resources/view/fchomo/node.js:1473 +#: htdocs/luci-static/resources/view/fchomo/node.js:1175 +#: htdocs/luci-static/resources/view/fchomo/node.js:1490 msgid "Routing mark" msgstr "路由标记" @@ -2246,7 +2262,7 @@ msgstr "路由模式" msgid "Routing mode of the traffic enters mihomo via firewall rules." msgstr "流量通过防火墙规则进入 mihomo 的路由模式。" -#: htdocs/luci-static/resources/view/fchomo/global.js:847 +#: htdocs/luci-static/resources/view/fchomo/global.js:862 msgid "Routing mode will be handle domain." msgstr "路由模式将处理域名。" @@ -2255,8 +2271,8 @@ msgstr "路由模式将处理域名。" msgid "Routing ports" msgstr "路由端口" -#: htdocs/luci-static/resources/view/fchomo/client.js:1066 -#: htdocs/luci-static/resources/view/fchomo/client.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:1072 +#: htdocs/luci-static/resources/view/fchomo/client.js:1081 msgid "Routing rule" msgstr "路由规则" @@ -2272,13 +2288,13 @@ msgstr "路由表 ID" msgid "Rule" msgstr "规则" -#: htdocs/luci-static/resources/view/fchomo/client.js:1607 -#: htdocs/luci-static/resources/view/fchomo/client.js:1620 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:133 +#: htdocs/luci-static/resources/view/fchomo/client.js:1597 +#: htdocs/luci-static/resources/view/fchomo/client.js:1610 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:135 msgid "Rule set" msgstr "规则集" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:357 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:355 msgid "Rule set URL" msgstr "规则集订阅 URL" @@ -2286,11 +2302,11 @@ msgstr "规则集订阅 URL" msgid "Ruleset" msgstr "规则集" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:185 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:183 msgid "Ruleset-URI-Scheme" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1038 +#: htdocs/luci-static/resources/fchomo.js:1088 msgid "Running" msgstr "正在运行" @@ -2314,16 +2330,16 @@ msgstr "" msgid "STUN ports" msgstr "STUN 端口" -#: htdocs/luci-static/resources/view/fchomo/client.js:1149 +#: htdocs/luci-static/resources/view/fchomo/client.js:1151 msgid "SUB-RULE" msgstr "SUB-RULE" -#: htdocs/luci-static/resources/view/fchomo/node.js:724 +#: htdocs/luci-static/resources/view/fchomo/node.js:745 msgid "SUoT version" msgstr "SUoT 版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:291 -#: htdocs/luci-static/resources/view/fchomo/server.js:283 +#: htdocs/luci-static/resources/view/fchomo/node.js:293 +#: htdocs/luci-static/resources/view/fchomo/server.js:250 msgid "Salamander" msgstr "Salamander" @@ -2356,19 +2372,19 @@ msgstr "以 50% 的概率发送随机 0-3333 字节的填充。" msgid "Send random ticket of 300s-600s duration for client 0-RTT reuse." msgstr "发送 300-600 秒的随机票证,以供客户端 0-RTT 重用。" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 -#: htdocs/luci-static/resources/view/fchomo/server.js:602 -#: htdocs/luci-static/resources/view/fchomo/server.js:793 -#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/server.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:588 +#: htdocs/luci-static/resources/view/fchomo/server.js:779 +#: htdocs/luci-static/resources/view/fchomo/server.js:794 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 msgid "Server" msgstr "服务端" -#: htdocs/luci-static/resources/view/fchomo/node.js:242 +#: htdocs/luci-static/resources/view/fchomo/node.js:244 msgid "Server address" msgstr "服务器地址" -#: htdocs/luci-static/resources/view/fchomo/node.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:1020 msgid "Server hostname" msgstr "服务器主机名称" @@ -2385,22 +2401,22 @@ msgstr "服务状态" msgid "Shadowsocks" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:506 -#: htdocs/luci-static/resources/view/fchomo/server.js:465 +#: htdocs/luci-static/resources/view/fchomo/node.js:527 +#: htdocs/luci-static/resources/view/fchomo/server.js:451 msgid "Shadowsocks chipher" msgstr "Shadowsocks 加密方法" -#: htdocs/luci-static/resources/view/fchomo/node.js:501 -#: htdocs/luci-static/resources/view/fchomo/server.js:460 +#: htdocs/luci-static/resources/view/fchomo/node.js:522 +#: htdocs/luci-static/resources/view/fchomo/server.js:446 msgid "Shadowsocks encrypt" msgstr "Shadowsocks 加密" -#: htdocs/luci-static/resources/view/fchomo/node.js:514 -#: htdocs/luci-static/resources/view/fchomo/server.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:535 +#: htdocs/luci-static/resources/view/fchomo/server.js:459 msgid "Shadowsocks password" msgstr "Shadowsocks 密码" -#: htdocs/luci-static/resources/view/fchomo/node.js:1109 +#: htdocs/luci-static/resources/view/fchomo/node.js:1130 msgid "Show connections in the dashboard for breaking connections easier." msgstr "在面板中显示连接以便于打断连接。" @@ -2412,14 +2428,14 @@ msgstr "静音" msgid "Simple round-robin all nodes" msgstr "简单轮替所有节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:1359 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:363 +#: htdocs/luci-static/resources/view/fchomo/node.js:1376 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:361 msgid "Size limit" msgstr "大小限制" -#: htdocs/luci-static/resources/view/fchomo/client.js:1455 -#: htdocs/luci-static/resources/view/fchomo/node.js:882 -#: htdocs/luci-static/resources/view/fchomo/node.js:1451 +#: htdocs/luci-static/resources/view/fchomo/client.js:1449 +#: htdocs/luci-static/resources/view/fchomo/node.js:903 +#: htdocs/luci-static/resources/view/fchomo/node.js:1468 msgid "Skip cert verify" msgstr "跳过证书验证" @@ -2470,22 +2486,22 @@ msgstr "Steam 客户端 端口" msgid "Steam P2P ports" msgstr "Steam P2P 端口" -#: htdocs/luci-static/resources/view/fchomo/client.js:1026 -#: htdocs/luci-static/resources/view/fchomo/client.js:1028 +#: htdocs/luci-static/resources/view/fchomo/client.js:1032 +#: htdocs/luci-static/resources/view/fchomo/client.js:1034 msgid "Strategy" msgstr "策略" -#: htdocs/luci-static/resources/view/fchomo/client.js:1176 -#: htdocs/luci-static/resources/view/fchomo/client.js:1185 +#: htdocs/luci-static/resources/view/fchomo/client.js:1178 +#: htdocs/luci-static/resources/view/fchomo/client.js:1187 msgid "Sub rule" msgstr "子规则" -#: htdocs/luci-static/resources/view/fchomo/client.js:1235 +#: htdocs/luci-static/resources/view/fchomo/client.js:1233 msgid "Sub rule group" msgstr "子规则组" -#: htdocs/luci-static/resources/fchomo.js:573 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:209 +#: htdocs/luci-static/resources/fchomo.js:643 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:207 msgid "Successfully imported %s %s of total %s." msgstr "已成功导入 %s 个%s (共 %s 个)。" @@ -2493,7 +2509,7 @@ msgstr "已成功导入 %s 个%s (共 %s 个)。" msgid "Successfully updated." msgstr "更新成功。" -#: htdocs/luci-static/resources/fchomo.js:1518 +#: htdocs/luci-static/resources/fchomo.js:1568 msgid "Successfully uploaded." msgstr "已成功上传。" @@ -2502,7 +2518,7 @@ msgstr "已成功上传。" msgid "Sudoku" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:182 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:180 msgid "" "Supports rule-set links of type: %s and format: %s." "
" @@ -2514,7 +2530,7 @@ msgstr "" msgid "System" msgstr "系统" -#: htdocs/luci-static/resources/view/fchomo/client.js:54 +#: htdocs/luci-static/resources/view/fchomo/client.js:56 msgid "System DNS" msgstr "系统 DNS" @@ -2532,8 +2548,8 @@ msgstr "系统 DNS" #: htdocs/luci-static/resources/fchomo.js:167 #: htdocs/luci-static/resources/fchomo.js:168 #: htdocs/luci-static/resources/fchomo.js:173 -#: htdocs/luci-static/resources/view/fchomo/client.js:517 -#: htdocs/luci-static/resources/view/fchomo/client.js:607 +#: htdocs/luci-static/resources/view/fchomo/client.js:527 +#: htdocs/luci-static/resources/view/fchomo/client.js:617 msgid "TCP" msgstr "TCP" @@ -2541,7 +2557,7 @@ msgstr "TCP" msgid "TCP concurrency" msgstr "TCP 并发" -#: htdocs/luci-static/resources/view/fchomo/node.js:1102 +#: htdocs/luci-static/resources/view/fchomo/node.js:1123 msgid "TCP only" msgstr "仅 TCP" @@ -2564,29 +2580,29 @@ msgstr "TCP-Keep-Alive 间隔" msgid "TCP/UDP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1133 -#: htdocs/luci-static/resources/view/fchomo/node.js:1418 +#: htdocs/luci-static/resources/view/fchomo/node.js:1154 +#: htdocs/luci-static/resources/view/fchomo/node.js:1435 msgid "TFO" msgstr "TCP 快速打开 (TFO)" #: htdocs/luci-static/resources/view/fchomo/global.js:529 -#: htdocs/luci-static/resources/view/fchomo/node.js:673 -#: htdocs/luci-static/resources/view/fchomo/node.js:799 -#: htdocs/luci-static/resources/view/fchomo/server.js:756 +#: htdocs/luci-static/resources/view/fchomo/node.js:694 +#: htdocs/luci-static/resources/view/fchomo/node.js:820 +#: htdocs/luci-static/resources/view/fchomo/server.js:742 msgid "TLS" msgstr "TLS" -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:851 +#: htdocs/luci-static/resources/view/fchomo/server.js:773 msgid "TLS ALPN" msgstr "TLS ALPN" -#: htdocs/luci-static/resources/view/fchomo/node.js:824 +#: htdocs/luci-static/resources/view/fchomo/node.js:845 msgid "TLS SNI" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:222 -#: htdocs/luci-static/resources/view/fchomo/server.js:206 +#: htdocs/luci-static/resources/view/fchomo/node.js:224 +#: htdocs/luci-static/resources/view/fchomo/server.js:173 msgid "TLS fields" msgstr "TLS字段" @@ -2599,39 +2615,39 @@ msgstr "" msgid "TURN ports" msgstr "TURN 端口" -#: htdocs/luci-static/resources/view/fchomo/server.js:276 +#: htdocs/luci-static/resources/view/fchomo/server.js:243 msgid "" "Tell the client to use the BBR flow control algorithm instead of Hysteria CC." msgstr "让客户端使用 BBR 流控算法。" -#: htdocs/luci-static/resources/view/fchomo/node.js:596 -#: htdocs/luci-static/resources/view/fchomo/node.js:603 +#: htdocs/luci-static/resources/view/fchomo/node.js:617 +#: htdocs/luci-static/resources/view/fchomo/node.js:624 msgid "The %s address used by local machine in the Wireguard network." msgstr "WireGuard 网络中使用的本机 %s 地址。" -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 +#: htdocs/luci-static/resources/view/fchomo/server.js:794 msgid "The %s private key, in PEM format." msgstr "%s私钥,需要 PEM 格式。" #: htdocs/luci-static/resources/view/fchomo/global.js:556 -#: htdocs/luci-static/resources/view/fchomo/node.js:891 -#: htdocs/luci-static/resources/view/fchomo/server.js:793 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/view/fchomo/node.js:912 +#: htdocs/luci-static/resources/view/fchomo/server.js:779 +#: htdocs/luci-static/resources/view/fchomo/server.js:817 msgid "The %s public key, in PEM format." msgstr "%s公钥,需要 PEM 格式。" -#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/node.js:946 msgid "" "The ECH parameter of the HTTPS record for the domain. Leave empty to resolve " "via DNS." msgstr "域名的 HTTPS 记录的 ECH 参数。留空则通过 DNS 解析。" -#: htdocs/luci-static/resources/view/fchomo/node.js:372 +#: htdocs/luci-static/resources/view/fchomo/node.js:374 msgid "The ED25519 available private key or UUID provided by Sudoku server." msgstr "Sudoku 服务器提供的 ED25519 可用私钥 或 UUID。" -#: htdocs/luci-static/resources/view/fchomo/server.js:333 +#: htdocs/luci-static/resources/view/fchomo/server.js:300 msgid "The ED25519 master public key or UUID generated by Sudoku." msgstr "Sudoku 生成的 ED25519 主公钥 或 UUID。" @@ -2639,42 +2655,42 @@ msgstr "Sudoku 生成的 ED25519 主公钥 或 UUID。" msgid "The default value is 2:00 every day." msgstr "默认值为每天 2:00。" -#: htdocs/luci-static/resources/view/fchomo/node.js:780 -#: htdocs/luci-static/resources/view/fchomo/server.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:801 +#: htdocs/luci-static/resources/view/fchomo/server.js:620 msgid "" "The first padding must have a probability of 100% and at least 35 bytes." msgstr "首个填充必须为 100% 的概率并且至少 35 字节。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1666 +#: htdocs/luci-static/resources/view/fchomo/client.js:1656 msgid "The matching %s will be deemed as not-poisoned." msgstr "匹配 %s 的将被视为未被投毒污染。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1675 -#: htdocs/luci-static/resources/view/fchomo/client.js:1679 -#: htdocs/luci-static/resources/view/fchomo/client.js:1684 +#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1669 +#: htdocs/luci-static/resources/view/fchomo/client.js:1674 msgid "The matching %s will be deemed as poisoned." msgstr "匹配 %s 的将被视为已被投毒污染。" -#: htdocs/luci-static/resources/view/fchomo/node.js:778 -#: htdocs/luci-static/resources/view/fchomo/server.js:632 +#: htdocs/luci-static/resources/view/fchomo/node.js:799 +#: htdocs/luci-static/resources/view/fchomo/server.js:618 msgid "The server and client can set different padding parameters." msgstr "服务器和客户端可以设置不同的填充参数。" #: htdocs/luci-static/resources/view/fchomo/global.js:600 -#: htdocs/luci-static/resources/view/fchomo/server.js:889 +#: htdocs/luci-static/resources/view/fchomo/server.js:875 msgid "This ECH parameter needs to be added to the HTTPS record of the domain." msgstr "此 ECH 参数需要添加到域名的 HTTPS 记录中。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1458 -#: htdocs/luci-static/resources/view/fchomo/node.js:885 -#: htdocs/luci-static/resources/view/fchomo/node.js:1454 +#: htdocs/luci-static/resources/view/fchomo/client.js:1452 +#: htdocs/luci-static/resources/view/fchomo/node.js:906 +#: htdocs/luci-static/resources/view/fchomo/node.js:1471 msgid "" "This is DANGEROUS, your traffic is almost like " "PLAIN TEXT! Use at your own risk!" msgstr "" "这是危险行为,您的流量将几乎等同于明文!使用风险自负!" -#: htdocs/luci-static/resources/view/fchomo/node.js:457 +#: htdocs/luci-static/resources/view/fchomo/node.js:478 msgid "" "This is the TUIC port of the SUoT protocol, designed to provide a QUIC " "stream based UDP relay mode that TUIC does not provide." @@ -2694,7 +2710,7 @@ msgid "" msgstr "" "要启用 Tun 支持,您需要安装 ip-fullkmod-tun。" -#: htdocs/luci-static/resources/view/fchomo/global.js:852 +#: htdocs/luci-static/resources/view/fchomo/global.js:867 msgid "To enable, you need to install dnsmasq-full." msgstr "要启用,您需要安装 dnsmasq-full。" @@ -2706,32 +2722,32 @@ msgstr "Tproxy Fwmark/fwmask" msgid "Tproxy port" msgstr "Tproxy 端口" -#: htdocs/luci-static/resources/view/fchomo/node.js:1610 +#: htdocs/luci-static/resources/view/fchomo/node.js:1627 msgid "Transit proxy group" msgstr "中转代理组" -#: htdocs/luci-static/resources/view/fchomo/node.js:1616 +#: htdocs/luci-static/resources/view/fchomo/node.js:1633 msgid "Transit proxy node" msgstr "中转代理节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:347 -#: htdocs/luci-static/resources/view/fchomo/node.js:962 -#: htdocs/luci-static/resources/view/fchomo/server.js:320 -#: htdocs/luci-static/resources/view/fchomo/server.js:940 +#: htdocs/luci-static/resources/view/fchomo/node.js:349 +#: htdocs/luci-static/resources/view/fchomo/node.js:983 +#: htdocs/luci-static/resources/view/fchomo/server.js:287 +#: htdocs/luci-static/resources/view/fchomo/server.js:926 msgid "Transport" msgstr "传输层" -#: htdocs/luci-static/resources/view/fchomo/node.js:223 -#: htdocs/luci-static/resources/view/fchomo/server.js:207 +#: htdocs/luci-static/resources/view/fchomo/node.js:225 +#: htdocs/luci-static/resources/view/fchomo/server.js:174 msgid "Transport fields" msgstr "传输层字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:967 -#: htdocs/luci-static/resources/view/fchomo/server.js:945 +#: htdocs/luci-static/resources/view/fchomo/node.js:988 +#: htdocs/luci-static/resources/view/fchomo/server.js:931 msgid "Transport type" msgstr "传输层类型" -#: htdocs/luci-static/resources/view/fchomo/client.js:732 +#: htdocs/luci-static/resources/view/fchomo/client.js:742 msgid "Treat the destination IP as the source IP." msgstr "将 目标 IP 视为 来源 IP。" @@ -2756,16 +2772,16 @@ msgstr "Tun 设置" msgid "Tun stack." msgstr "Tun 堆栈" -#: htdocs/luci-static/resources/view/fchomo/client.js:458 -#: htdocs/luci-static/resources/view/fchomo/client.js:571 -#: htdocs/luci-static/resources/view/fchomo/client.js:665 -#: htdocs/luci-static/resources/view/fchomo/client.js:910 -#: htdocs/luci-static/resources/view/fchomo/client.js:1604 -#: htdocs/luci-static/resources/view/fchomo/node.js:236 -#: htdocs/luci-static/resources/view/fchomo/node.js:1296 -#: htdocs/luci-static/resources/view/fchomo/node.js:1581 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 -#: htdocs/luci-static/resources/view/fchomo/server.js:226 +#: htdocs/luci-static/resources/view/fchomo/client.js:468 +#: htdocs/luci-static/resources/view/fchomo/client.js:581 +#: htdocs/luci-static/resources/view/fchomo/client.js:675 +#: htdocs/luci-static/resources/view/fchomo/client.js:916 +#: htdocs/luci-static/resources/view/fchomo/client.js:1594 +#: htdocs/luci-static/resources/view/fchomo/node.js:238 +#: htdocs/luci-static/resources/view/fchomo/node.js:1313 +#: htdocs/luci-static/resources/view/fchomo/node.js:1598 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:261 +#: htdocs/luci-static/resources/view/fchomo/server.js:193 msgid "Type" msgstr "类型" @@ -2774,11 +2790,11 @@ msgstr "类型" #: htdocs/luci-static/resources/fchomo.js:170 #: htdocs/luci-static/resources/fchomo.js:171 #: htdocs/luci-static/resources/fchomo.js:172 -#: htdocs/luci-static/resources/view/fchomo/client.js:516 -#: htdocs/luci-static/resources/view/fchomo/client.js:606 -#: htdocs/luci-static/resources/view/fchomo/node.js:713 -#: htdocs/luci-static/resources/view/fchomo/node.js:1428 -#: htdocs/luci-static/resources/view/fchomo/server.js:539 +#: htdocs/luci-static/resources/view/fchomo/client.js:526 +#: htdocs/luci-static/resources/view/fchomo/client.js:616 +#: htdocs/luci-static/resources/view/fchomo/node.js:734 +#: htdocs/luci-static/resources/view/fchomo/node.js:1445 +#: htdocs/luci-static/resources/view/fchomo/server.js:525 msgid "UDP" msgstr "UDP" @@ -2786,19 +2802,19 @@ msgstr "UDP" msgid "UDP NAT expiration time" msgstr "UDP NAT 过期时间" -#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 msgid "UDP over stream" msgstr "UDP over stream" -#: htdocs/luci-static/resources/view/fchomo/node.js:462 +#: htdocs/luci-static/resources/view/fchomo/node.js:483 msgid "UDP over stream version" msgstr "UDP over stream 版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:449 +#: htdocs/luci-static/resources/view/fchomo/node.js:470 msgid "UDP packet relay mode." msgstr "UDP 包中继模式。" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 +#: htdocs/luci-static/resources/view/fchomo/node.js:469 msgid "UDP relay mode" msgstr "UDP 中继模式" @@ -2806,15 +2822,15 @@ msgstr "UDP 中继模式" msgid "URL test" msgstr "自动选择" -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/node.js:545 -#: htdocs/luci-static/resources/view/fchomo/server.js:330 -#: htdocs/luci-static/resources/view/fchomo/server.js:424 -#: htdocs/luci-static/resources/view/fchomo/server.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:448 +#: htdocs/luci-static/resources/view/fchomo/node.js:566 +#: htdocs/luci-static/resources/view/fchomo/server.js:297 +#: htdocs/luci-static/resources/view/fchomo/server.js:410 +#: htdocs/luci-static/resources/view/fchomo/server.js:474 msgid "UUID" msgstr "UUID" -#: htdocs/luci-static/resources/fchomo.js:1083 +#: htdocs/luci-static/resources/fchomo.js:1133 msgid "Unable to download unsupported type: %s" msgstr "无法下载不支持的类型: %s" @@ -2839,8 +2855,8 @@ msgstr "未知错误。" msgid "Unknown error: %s" msgstr "未知错误:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:718 -#: htdocs/luci-static/resources/view/fchomo/node.js:1433 +#: htdocs/luci-static/resources/view/fchomo/node.js:739 +#: htdocs/luci-static/resources/view/fchomo/node.js:1450 msgid "UoT" msgstr "UDP over TCP (UoT)" @@ -2848,22 +2864,29 @@ msgstr "UDP over TCP (UoT)" msgid "Update failed." msgstr "更新失败。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1365 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:369 +#: htdocs/luci-static/resources/view/fchomo/node.js:1382 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:367 msgid "Update interval" msgstr "更新间隔" -#: htdocs/luci-static/resources/view/fchomo/node.js:1120 +#: htdocs/luci-static/resources/view/fchomo/node.js:426 +#: htdocs/luci-static/resources/view/fchomo/server.js:404 +msgid "" +"Uplink keeps the Sudoku protocol, and downlink characteristics are " +"consistent with uplink characteristics." +msgstr "上行链路保持数独协议,下行链路特性与上行链路特性一致。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1141 msgid "Upload bandwidth" msgstr "上传带宽" -#: htdocs/luci-static/resources/view/fchomo/node.js:1121 +#: htdocs/luci-static/resources/view/fchomo/node.js:1142 msgid "Upload bandwidth in Mbps." msgstr "上传带宽(单位:Mbps)。" -#: htdocs/luci-static/resources/view/fchomo/node.js:896 -#: htdocs/luci-static/resources/view/fchomo/server.js:799 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 +#: htdocs/luci-static/resources/view/fchomo/node.js:917 +#: htdocs/luci-static/resources/view/fchomo/server.js:785 +#: htdocs/luci-static/resources/view/fchomo/server.js:825 msgid "Upload certificate" msgstr "上传证书" @@ -2871,41 +2894,41 @@ msgstr "上传证书" msgid "Upload initial package" msgstr "上传初始资源包" -#: htdocs/luci-static/resources/view/fchomo/node.js:910 -#: htdocs/luci-static/resources/view/fchomo/server.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/server.js:800 msgid "Upload key" msgstr "上传密钥" #: htdocs/luci-static/resources/view/fchomo/global.js:306 -#: htdocs/luci-static/resources/view/fchomo/node.js:899 -#: htdocs/luci-static/resources/view/fchomo/node.js:913 -#: htdocs/luci-static/resources/view/fchomo/server.js:802 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:842 +#: htdocs/luci-static/resources/view/fchomo/node.js:920 +#: htdocs/luci-static/resources/view/fchomo/node.js:934 +#: htdocs/luci-static/resources/view/fchomo/server.js:788 +#: htdocs/luci-static/resources/view/fchomo/server.js:803 +#: htdocs/luci-static/resources/view/fchomo/server.js:828 msgid "Upload..." msgstr "上传..." -#: htdocs/luci-static/resources/view/fchomo/client.js:1276 +#: htdocs/luci-static/resources/view/fchomo/client.js:1274 msgid "" "Used to resolve domains that can be directly connected. Can use domestic DNS " "servers or ECS." msgstr "用于解析可直连的域名。可以使用国内 DNS 服务器或 ECS。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1278 +#: htdocs/luci-static/resources/view/fchomo/client.js:1276 msgid "" "Used to resolve domains you want to proxy. Recommended to configure %s for " "DNS servers." msgstr "用于解析想要代理的域名。建议为 DNS 服务器配置%s。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1260 +#: htdocs/luci-static/resources/view/fchomo/client.js:1258 msgid "Used to resolve the domain of the DNS server. Must be IP." msgstr "用于解析 DNS 服务器的域名。必须是 IP。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1267 +#: htdocs/luci-static/resources/view/fchomo/client.js:1265 msgid "Used to resolve the domain of the Proxy node." msgstr "用于解析代理节点的域名。" -#: htdocs/luci-static/resources/view/fchomo/node.js:825 +#: htdocs/luci-static/resources/view/fchomo/node.js:846 msgid "Used to verify the hostname on the returned certificates." msgstr "用于验证返回的证书上的主机名。" @@ -2913,8 +2936,8 @@ msgstr "用于验证返回的证书上的主机名。" msgid "User Authentication" msgstr "用户认证" -#: htdocs/luci-static/resources/view/fchomo/node.js:254 -#: htdocs/luci-static/resources/view/fchomo/server.js:249 +#: htdocs/luci-static/resources/view/fchomo/node.js:256 +#: htdocs/luci-static/resources/view/fchomo/server.js:216 msgid "Username" msgstr "用户名" @@ -2922,11 +2945,11 @@ msgstr "用户名" msgid "Users filter mode" msgstr "使用者过滤模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:1050 +#: htdocs/luci-static/resources/view/fchomo/node.js:1071 msgid "V2ray HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +#: htdocs/luci-static/resources/view/fchomo/node.js:1076 msgid "V2ray HTTPUpgrade fast open" msgstr "" @@ -2940,9 +2963,9 @@ msgstr "" msgid "VMess" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1302 -#: htdocs/luci-static/resources/view/fchomo/node.js:1587 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:306 +#: htdocs/luci-static/resources/view/fchomo/node.js:1319 +#: htdocs/luci-static/resources/view/fchomo/node.js:1604 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:304 msgid "Value" msgstr "可视化值" @@ -2950,18 +2973,18 @@ msgstr "可视化值" msgid "Verify if given" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:418 -#: htdocs/luci-static/resources/view/fchomo/node.js:692 -#: htdocs/luci-static/resources/view/fchomo/server.js:530 +#: htdocs/luci-static/resources/view/fchomo/node.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:713 +#: htdocs/luci-static/resources/view/fchomo/server.js:516 msgid "Version" msgstr "版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:700 +#: htdocs/luci-static/resources/view/fchomo/node.js:721 msgid "Version hint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:221 -#: htdocs/luci-static/resources/view/fchomo/server.js:205 +#: htdocs/luci-static/resources/view/fchomo/node.js:223 +#: htdocs/luci-static/resources/view/fchomo/server.js:172 msgid "Vless Encryption fields" msgstr "Vless Encryption 字段" @@ -2973,16 +2996,24 @@ msgstr "以 75% 的概率等待随机 0-111 毫秒。" msgid "Warning" msgstr "警告" -#: htdocs/luci-static/resources/view/fchomo/node.js:972 -#: htdocs/luci-static/resources/view/fchomo/node.js:983 -#: htdocs/luci-static/resources/view/fchomo/node.js:988 -#: htdocs/luci-static/resources/view/fchomo/server.js:947 -#: htdocs/luci-static/resources/view/fchomo/server.js:958 -#: htdocs/luci-static/resources/view/fchomo/server.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:993 +#: htdocs/luci-static/resources/view/fchomo/node.js:1004 +#: htdocs/luci-static/resources/view/fchomo/node.js:1009 +#: htdocs/luci-static/resources/view/fchomo/server.js:933 +#: htdocs/luci-static/resources/view/fchomo/server.js:944 +#: htdocs/luci-static/resources/view/fchomo/server.js:949 msgid "WebSocket" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:165 +#: htdocs/luci-static/resources/view/fchomo/node.js:425 +#: htdocs/luci-static/resources/view/fchomo/server.js:403 +msgid "" +"When disabled, downlink ciphertext is split into 6-bit segments, reusing the " +"original padding pool and obfuscate type to reduce downlink overhead." +msgstr "" +"禁用后,下行密文将分成 6 位片段,重用原始填充池和混淆类型,以减少下行开销。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:132 msgid "When used as a server, HomeProxy is a better choice." msgstr "用作服务端时,HomeProxy 是更好的选择。" @@ -2994,27 +3025,27 @@ msgstr "白名单" msgid "WireGuard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:617 +#: htdocs/luci-static/resources/view/fchomo/node.js:638 msgid "WireGuard peer public key." msgstr "WireGuard 对端公钥。" -#: htdocs/luci-static/resources/view/fchomo/node.js:624 +#: htdocs/luci-static/resources/view/fchomo/node.js:645 msgid "WireGuard pre-shared key." msgstr "WireGuard 预共享密钥。" -#: htdocs/luci-static/resources/view/fchomo/node.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:630 msgid "WireGuard requires base64-encoded private keys." msgstr "WireGuard 要求 base64 编码的私钥。" -#: htdocs/luci-static/resources/view/fchomo/server.js:593 +#: htdocs/luci-static/resources/view/fchomo/server.js:579 msgid "XOR mode" msgstr "XOR 模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:590 +#: htdocs/luci-static/resources/view/fchomo/node.js:611 msgid "Xudp (Xray-core)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:285 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:283 msgid "Yaml text" msgstr "Yaml 格式文本" @@ -3022,14 +3053,14 @@ msgstr "Yaml 格式文本" msgid "YouTube" msgstr "油管" -#: htdocs/luci-static/resources/fchomo.js:1500 +#: htdocs/luci-static/resources/fchomo.js:1550 msgid "Your %s was successfully uploaded. Size: %sB." msgstr "您的 %s 已成功上传。大小:%sB。" #: htdocs/luci-static/resources/fchomo.js:289 #: htdocs/luci-static/resources/fchomo.js:302 #: htdocs/luci-static/resources/fchomo.js:307 -#: htdocs/luci-static/resources/view/fchomo/node.js:570 +#: htdocs/luci-static/resources/view/fchomo/node.js:591 msgid "aes-128-gcm" msgstr "" @@ -3042,18 +3073,18 @@ msgstr "" msgid "aes-256-gcm" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:567 +#: htdocs/luci-static/resources/view/fchomo/node.js:588 msgid "auto" msgstr "自动" -#: htdocs/luci-static/resources/view/fchomo/node.js:444 -#: htdocs/luci-static/resources/view/fchomo/server.js:435 +#: htdocs/luci-static/resources/view/fchomo/node.js:465 +#: htdocs/luci-static/resources/view/fchomo/server.js:421 msgid "bbr" msgstr "bbr" -#: htdocs/luci-static/resources/view/fchomo/node.js:901 -#: htdocs/luci-static/resources/view/fchomo/server.js:804 -#: htdocs/luci-static/resources/view/fchomo/server.js:844 +#: htdocs/luci-static/resources/view/fchomo/node.js:922 +#: htdocs/luci-static/resources/view/fchomo/server.js:790 +#: htdocs/luci-static/resources/view/fchomo/server.js:830 msgid "certificate" msgstr "证书" @@ -3063,17 +3094,17 @@ msgstr "证书" msgid "chacha20-ietf-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:571 +#: htdocs/luci-static/resources/view/fchomo/node.js:592 msgid "chacha20-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:442 -#: htdocs/luci-static/resources/view/fchomo/server.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:463 +#: htdocs/luci-static/resources/view/fchomo/server.js:419 msgid "cubic" msgstr "cubic" -#: htdocs/luci-static/resources/view/fchomo/server.js:545 -#: htdocs/luci-static/resources/view/fchomo/server.js:576 +#: htdocs/luci-static/resources/view/fchomo/server.js:531 +#: htdocs/luci-static/resources/view/fchomo/server.js:562 msgid "decryption" msgstr "decryption" @@ -3081,27 +3112,27 @@ msgstr "decryption" msgid "dnsmasq selects upstream on its own. (may affect CDN accuracy)" msgstr "dnsmasq 自行选择上游服务器。 (可能影响 CDN 准确性)" -#: htdocs/luci-static/resources/view/fchomo/node.js:1445 +#: htdocs/luci-static/resources/view/fchomo/node.js:1462 msgid "down" msgstr "Hysteria 下载速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:732 -#: htdocs/luci-static/resources/view/fchomo/node.js:755 -#: htdocs/luci-static/resources/view/fchomo/server.js:580 +#: htdocs/luci-static/resources/view/fchomo/node.js:753 +#: htdocs/luci-static/resources/view/fchomo/node.js:776 +#: htdocs/luci-static/resources/view/fchomo/server.js:566 msgid "encryption" msgstr "encryption" -#: htdocs/luci-static/resources/view/fchomo/node.js:971 -#: htdocs/luci-static/resources/view/fchomo/node.js:982 -#: htdocs/luci-static/resources/view/fchomo/node.js:987 -#: htdocs/luci-static/resources/view/fchomo/server.js:946 -#: htdocs/luci-static/resources/view/fchomo/server.js:957 -#: htdocs/luci-static/resources/view/fchomo/server.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:992 +#: htdocs/luci-static/resources/view/fchomo/node.js:1003 +#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/server.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:943 +#: htdocs/luci-static/resources/view/fchomo/server.js:948 msgid "gRPC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 -#: htdocs/luci-static/resources/view/fchomo/server.js:981 +#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +#: htdocs/luci-static/resources/view/fchomo/server.js:967 msgid "gRPC service name" msgstr "gRPC 服务名称" @@ -3109,11 +3140,11 @@ msgstr "gRPC 服务名称" msgid "gVisor" msgstr "gVisor" -#: htdocs/luci-static/resources/view/fchomo/node.js:1071 +#: htdocs/luci-static/resources/view/fchomo/node.js:1092 msgid "h2mux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:745 +#: htdocs/luci-static/resources/view/fchomo/server.js:731 msgid "least one keypair required" msgstr "至少需要一对密钥" @@ -3121,13 +3152,13 @@ msgstr "至少需要一对密钥" msgid "metacubexd" msgstr "metacubexd" -#: htdocs/luci-static/resources/view/fchomo/client.js:883 -#: htdocs/luci-static/resources/view/fchomo/client.js:1122 -#: htdocs/luci-static/resources/view/fchomo/client.js:1218 -#: htdocs/luci-static/resources/view/fchomo/client.js:1359 -#: htdocs/luci-static/resources/view/fchomo/client.js:1581 -#: htdocs/luci-static/resources/view/fchomo/node.js:1268 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:226 +#: htdocs/luci-static/resources/view/fchomo/client.js:889 +#: htdocs/luci-static/resources/view/fchomo/client.js:1124 +#: htdocs/luci-static/resources/view/fchomo/client.js:1216 +#: htdocs/luci-static/resources/view/fchomo/client.js:1353 +#: htdocs/luci-static/resources/view/fchomo/client.js:1571 +#: htdocs/luci-static/resources/view/fchomo/node.js:1285 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:224 msgid "mihomo config" msgstr "mihomo 配置" @@ -3135,35 +3166,35 @@ msgstr "mihomo 配置" msgid "mlkem768x25519plus" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1137 -#: htdocs/luci-static/resources/view/fchomo/node.js:1423 +#: htdocs/luci-static/resources/view/fchomo/node.js:1158 +#: htdocs/luci-static/resources/view/fchomo/node.js:1440 msgid "mpTCP" msgstr "多路径 TCP (mpTCP)" -#: htdocs/luci-static/resources/view/fchomo/node.js:443 -#: htdocs/luci-static/resources/view/fchomo/server.js:434 +#: htdocs/luci-static/resources/view/fchomo/node.js:464 +#: htdocs/luci-static/resources/view/fchomo/server.js:420 msgid "new_reno" msgstr "new_reno" -#: htdocs/luci-static/resources/view/fchomo/client.js:749 +#: htdocs/luci-static/resources/view/fchomo/client.js:759 msgid "no-resolve" msgstr "no-resolve" -#: htdocs/luci-static/resources/fchomo.js:1251 -#: htdocs/luci-static/resources/fchomo.js:1346 -#: htdocs/luci-static/resources/fchomo.js:1381 -#: htdocs/luci-static/resources/fchomo.js:1393 +#: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1396 +#: htdocs/luci-static/resources/fchomo.js:1431 +#: htdocs/luci-static/resources/fchomo.js:1443 msgid "non-empty value" msgstr "非空值" #: htdocs/luci-static/resources/fchomo.js:287 #: htdocs/luci-static/resources/fchomo.js:301 #: htdocs/luci-static/resources/fchomo.js:313 -#: htdocs/luci-static/resources/view/fchomo/node.js:568 -#: htdocs/luci-static/resources/view/fchomo/node.js:588 -#: htdocs/luci-static/resources/view/fchomo/node.js:661 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:302 -#: htdocs/luci-static/resources/view/fchomo/server.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:589 +#: htdocs/luci-static/resources/view/fchomo/node.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:682 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:300 +#: htdocs/luci-static/resources/view/fchomo/server.js:497 msgid "none" msgstr "无" @@ -3171,7 +3202,7 @@ msgstr "无" msgid "not found" msgstr "未找到" -#: htdocs/luci-static/resources/view/fchomo/client.js:900 +#: htdocs/luci-static/resources/view/fchomo/client.js:906 msgid "not included \",\"" msgstr "不包含 \",\"" @@ -3179,20 +3210,20 @@ msgstr "不包含 \",\"" msgid "null" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:662 +#: htdocs/luci-static/resources/view/fchomo/node.js:683 msgid "obfs-simple" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1403 +#: htdocs/luci-static/resources/view/fchomo/node.js:1420 msgid "override.proxy-name" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:589 +#: htdocs/luci-static/resources/view/fchomo/node.js:610 msgid "packet addr (v2ray-core v5+)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:915 -#: htdocs/luci-static/resources/view/fchomo/server.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:936 +#: htdocs/luci-static/resources/view/fchomo/server.js:805 msgid "private key" msgstr "私钥" @@ -3200,33 +3231,33 @@ msgstr "私钥" msgid "razord-meta" msgstr "razord-meta" -#: htdocs/luci-static/resources/view/fchomo/client.js:1055 #: htdocs/luci-static/resources/view/fchomo/client.js:1061 +#: htdocs/luci-static/resources/view/fchomo/client.js:1067 msgid "requires front-end adaptation using the API." msgstr "需要使用 API 的前端适配。" -#: htdocs/luci-static/resources/view/fchomo/node.js:666 +#: htdocs/luci-static/resources/view/fchomo/node.js:687 msgid "restls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:210 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:208 msgid "rule-set" msgstr "规则集" -#: htdocs/luci-static/resources/view/fchomo/node.js:665 -#: htdocs/luci-static/resources/view/fchomo/server.js:512 +#: htdocs/luci-static/resources/view/fchomo/node.js:686 +#: htdocs/luci-static/resources/view/fchomo/server.js:498 msgid "shadow-tls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1069 +#: htdocs/luci-static/resources/view/fchomo/node.js:1090 msgid "smux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:731 +#: htdocs/luci-static/resources/view/fchomo/client.js:741 msgid "src" msgstr "src" -#: htdocs/luci-static/resources/view/fchomo/server.js:329 +#: htdocs/luci-static/resources/view/fchomo/server.js:296 msgid "sudoku-keypair" msgstr "" @@ -3242,62 +3273,62 @@ msgstr "独立 UCI 标识" msgid "unique identifier" msgstr "独立标识" -#: htdocs/luci-static/resources/fchomo.js:1402 +#: htdocs/luci-static/resources/fchomo.js:1452 msgid "unique value" msgstr "独立值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1439 +#: htdocs/luci-static/resources/view/fchomo/node.js:1456 msgid "up" msgstr "Hysteria 上传速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:419 -#: htdocs/luci-static/resources/view/fchomo/node.js:463 -#: htdocs/luci-static/resources/view/fchomo/node.js:693 -#: htdocs/luci-static/resources/view/fchomo/node.js:725 -#: htdocs/luci-static/resources/view/fchomo/server.js:531 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 +#: htdocs/luci-static/resources/view/fchomo/node.js:484 +#: htdocs/luci-static/resources/view/fchomo/node.js:714 +#: htdocs/luci-static/resources/view/fchomo/node.js:746 +#: htdocs/luci-static/resources/view/fchomo/server.js:517 msgid "v1" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:420 -#: htdocs/luci-static/resources/view/fchomo/node.js:694 -#: htdocs/luci-static/resources/view/fchomo/node.js:726 -#: htdocs/luci-static/resources/view/fchomo/server.js:532 +#: htdocs/luci-static/resources/view/fchomo/node.js:441 +#: htdocs/luci-static/resources/view/fchomo/node.js:715 +#: htdocs/luci-static/resources/view/fchomo/node.js:747 +#: htdocs/luci-static/resources/view/fchomo/server.js:518 msgid "v2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:421 -#: htdocs/luci-static/resources/view/fchomo/node.js:695 -#: htdocs/luci-static/resources/view/fchomo/server.js:533 +#: htdocs/luci-static/resources/view/fchomo/node.js:442 +#: htdocs/luci-static/resources/view/fchomo/node.js:716 +#: htdocs/luci-static/resources/view/fchomo/server.js:519 msgid "v3" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1298 -#: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1348 +#: htdocs/luci-static/resources/fchomo.js:1351 msgid "valid JSON format" msgstr "有效的 JSON 格式" -#: htdocs/luci-static/resources/view/fchomo/node.js:875 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 msgid "valid SHA256 string with %d characters" msgstr "包含 %d 个字符的有效 SHA256 字符串" -#: htdocs/luci-static/resources/fchomo.js:1323 -#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1373 +#: htdocs/luci-static/resources/fchomo.js:1376 msgid "valid URL" msgstr "有效网址" -#: htdocs/luci-static/resources/fchomo.js:1336 +#: htdocs/luci-static/resources/fchomo.js:1386 msgid "valid base64 key with %d characters" msgstr "包含 %d 个字符的有效 base64 密钥" -#: htdocs/luci-static/resources/fchomo.js:1383 +#: htdocs/luci-static/resources/fchomo.js:1433 msgid "valid key length with %d characters" msgstr "包含 %d 个字符的有效密钥" -#: htdocs/luci-static/resources/fchomo.js:1261 +#: htdocs/luci-static/resources/fchomo.js:1311 msgid "valid port value" msgstr "有效端口值" -#: htdocs/luci-static/resources/fchomo.js:1311 +#: htdocs/luci-static/resources/fchomo.js:1361 msgid "valid uuid" msgstr "有效 uuid" @@ -3317,7 +3348,7 @@ msgstr "" msgid "yacd-meta" msgstr "yacd-meta" -#: htdocs/luci-static/resources/view/fchomo/node.js:1070 +#: htdocs/luci-static/resources/view/fchomo/node.js:1091 msgid "yamux" msgstr "" @@ -3325,11 +3356,11 @@ msgstr "" msgid "zashboard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:569 +#: htdocs/luci-static/resources/view/fchomo/node.js:590 msgid "zero" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1085 +#: htdocs/luci-static/resources/fchomo.js:1135 msgid "🡇" msgstr "" diff --git a/small/luci-app-fchomo/po/zh_Hant/fchomo.po b/small/luci-app-fchomo/po/zh_Hant/fchomo.po index 05335b9258..2a5856c7e8 100644 --- a/small/luci-app-fchomo/po/zh_Hant/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hant/fchomo.po @@ -12,36 +12,35 @@ msgstr "" msgid "%s log" msgstr "%s 日誌" -#: htdocs/luci-static/resources/fchomo.js:594 -#: htdocs/luci-static/resources/view/fchomo/client.js:237 -#: htdocs/luci-static/resources/view/fchomo/client.js:267 -#: htdocs/luci-static/resources/view/fchomo/client.js:363 +#: htdocs/luci-static/resources/fchomo.js:558 +#: htdocs/luci-static/resources/fchomo.js:561 +#: htdocs/luci-static/resources/view/fchomo/client.js:293 msgid "(Imported)" msgstr "(已導入)" #: htdocs/luci-static/resources/view/fchomo/global.js:549 #: htdocs/luci-static/resources/view/fchomo/global.js:555 -#: htdocs/luci-static/resources/view/fchomo/node.js:890 -#: htdocs/luci-static/resources/view/fchomo/node.js:896 -#: htdocs/luci-static/resources/view/fchomo/node.js:904 -#: htdocs/luci-static/resources/view/fchomo/node.js:910 -#: htdocs/luci-static/resources/view/fchomo/server.js:822 -#: htdocs/luci-static/resources/view/fchomo/server.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 +#: htdocs/luci-static/resources/view/fchomo/node.js:911 +#: htdocs/luci-static/resources/view/fchomo/node.js:917 +#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/server.js:816 +#: htdocs/luci-static/resources/view/fchomo/server.js:825 msgid "(mTLS)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:924 -#: htdocs/luci-static/resources/view/fchomo/client.js:925 -#: htdocs/luci-static/resources/view/fchomo/client.js:938 -#: htdocs/luci-static/resources/view/fchomo/client.js:939 -#: htdocs/luci-static/resources/view/fchomo/client.js:1151 -#: htdocs/luci-static/resources/view/fchomo/client.js:1622 -#: htdocs/luci-static/resources/view/fchomo/client.js:1623 -#: htdocs/luci-static/resources/view/fchomo/node.js:1591 -#: htdocs/luci-static/resources/view/fchomo/node.js:1597 -#: htdocs/luci-static/resources/view/fchomo/node.js:1611 -#: htdocs/luci-static/resources/view/fchomo/node.js:1617 +#: htdocs/luci-static/resources/view/fchomo/client.js:930 +#: htdocs/luci-static/resources/view/fchomo/client.js:931 +#: htdocs/luci-static/resources/view/fchomo/client.js:944 +#: htdocs/luci-static/resources/view/fchomo/client.js:945 +#: htdocs/luci-static/resources/view/fchomo/client.js:1153 +#: htdocs/luci-static/resources/view/fchomo/client.js:1612 +#: htdocs/luci-static/resources/view/fchomo/client.js:1613 +#: htdocs/luci-static/resources/view/fchomo/node.js:1608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1614 +#: htdocs/luci-static/resources/view/fchomo/node.js:1628 +#: htdocs/luci-static/resources/view/fchomo/node.js:1634 msgid "-- Please choose --" msgstr "-- 請選擇 --" @@ -70,15 +69,15 @@ msgstr "" msgid "2022-blake3-chacha20-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:617 +#: htdocs/luci-static/resources/view/fchomo/client.js:627 msgid "0 or 1 only." msgstr "僅限 01。" -#: htdocs/luci-static/resources/view/fchomo/node.js:897 -#: htdocs/luci-static/resources/view/fchomo/node.js:911 -#: htdocs/luci-static/resources/view/fchomo/server.js:800 -#: htdocs/luci-static/resources/view/fchomo/server.js:815 -#: htdocs/luci-static/resources/view/fchomo/server.js:840 +#: htdocs/luci-static/resources/view/fchomo/node.js:918 +#: htdocs/luci-static/resources/view/fchomo/node.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:786 +#: htdocs/luci-static/resources/view/fchomo/server.js:801 +#: htdocs/luci-static/resources/view/fchomo/server.js:826 msgid "Save your configuration before uploading files!" msgstr "上傳文件前請先保存配置!" @@ -135,56 +134,56 @@ msgstr "ASN 版本" msgid "Access Control" msgstr "訪問控制" -#: htdocs/luci-static/resources/view/fchomo/client.js:1551 +#: htdocs/luci-static/resources/view/fchomo/client.js:1545 msgid "Add a DNS policy" msgstr "新增 DNS 策略" -#: htdocs/luci-static/resources/view/fchomo/client.js:1314 +#: htdocs/luci-static/resources/view/fchomo/client.js:1312 msgid "Add a DNS server" msgstr "新增 DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/node.js:215 +#: htdocs/luci-static/resources/view/fchomo/node.js:217 msgid "Add a Node" msgstr "新增 節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 +#: htdocs/luci-static/resources/view/fchomo/node.js:1198 msgid "Add a provider" msgstr "新增 供應商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1567 +#: htdocs/luci-static/resources/view/fchomo/node.js:1584 msgid "Add a proxy chain" msgstr "新增 代理鏈" -#: htdocs/luci-static/resources/view/fchomo/client.js:819 +#: htdocs/luci-static/resources/view/fchomo/client.js:829 msgid "Add a proxy group" msgstr "新增 代理組" -#: htdocs/luci-static/resources/view/fchomo/client.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:1081 msgid "Add a routing rule" msgstr "新增 路由規則" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:133 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:135 msgid "Add a rule set" msgstr "新增 規則集" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 +#: htdocs/luci-static/resources/view/fchomo/server.js:166 msgid "Add a server" msgstr "新增 伺服器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1185 +#: htdocs/luci-static/resources/view/fchomo/client.js:1187 msgid "Add a sub rule" msgstr "新增 子規則" -#: htdocs/luci-static/resources/view/fchomo/node.js:1392 +#: htdocs/luci-static/resources/view/fchomo/node.js:1409 msgid "Add prefix" msgstr "添加前綴" -#: htdocs/luci-static/resources/view/fchomo/node.js:1396 +#: htdocs/luci-static/resources/view/fchomo/node.js:1413 msgid "Add suffix" msgstr "添加後綴" -#: htdocs/luci-static/resources/view/fchomo/client.js:1376 -#: htdocs/luci-static/resources/view/fchomo/client.js:1381 +#: htdocs/luci-static/resources/view/fchomo/client.js:1370 +#: htdocs/luci-static/resources/view/fchomo/client.js:1375 msgid "Address" msgstr "位址" @@ -201,7 +200,7 @@ msgstr "客戶端維護的 NAT 映射 的老化時間。
" #: htdocs/luci-static/resources/view/fchomo/global.js:779 #: htdocs/luci-static/resources/view/fchomo/global.js:842 -#: htdocs/luci-static/resources/view/fchomo/global.js:861 +#: htdocs/luci-static/resources/view/fchomo/global.js:876 msgid "All allowed" msgstr "允許所有" @@ -216,7 +215,7 @@ msgid "" msgstr "" "允許從私有網路訪問。
要從公共網站訪問私有網路上的 API,則必須啟用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:630 +#: htdocs/luci-static/resources/view/fchomo/node.js:651 msgid "Allowed IPs" msgstr "允許的 IP" @@ -228,8 +227,8 @@ msgstr "已是最新版本。" msgid "Already in updating." msgstr "已在更新中。" -#: htdocs/luci-static/resources/view/fchomo/node.js:559 -#: htdocs/luci-static/resources/view/fchomo/server.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:580 +#: htdocs/luci-static/resources/view/fchomo/server.js:488 msgid "Alter ID" msgstr "額外 ID" @@ -251,11 +250,11 @@ msgstr "作為 dnsmasq 的最優先上游" msgid "As the TOP upstream of dnsmasq." msgstr "作為 dnsmasq 的最優先上游。" -#: htdocs/luci-static/resources/view/fchomo/server.js:452 +#: htdocs/luci-static/resources/view/fchomo/server.js:438 msgid "Auth timeout" msgstr "認證超時" -#: htdocs/luci-static/resources/view/fchomo/node.js:581 +#: htdocs/luci-static/resources/view/fchomo/node.js:602 msgid "Authenticated length" msgstr "認證長度" @@ -263,7 +262,7 @@ msgstr "認證長度" msgid "Auto" msgstr "自動" -#: htdocs/luci-static/resources/view/fchomo/server.js:222 +#: htdocs/luci-static/resources/view/fchomo/server.js:189 msgid "Auto configure firewall" msgstr "自動配置防火牆" @@ -283,27 +282,27 @@ msgstr "百度" msgid "Based on google/gvisor." msgstr "基於 google/gvisor。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:269 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:267 msgid "Behavior" msgstr "行為" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:278 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:292 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:276 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:290 msgid "Binary format only supports domain / ipcidr" msgstr "二進位格式僅支持 domain/ipcidr" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:286 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 msgid "Binary mrs" msgstr "二進位 mrs" #: htdocs/luci-static/resources/view/fchomo/global.js:740 -#: htdocs/luci-static/resources/view/fchomo/node.js:1147 -#: htdocs/luci-static/resources/view/fchomo/node.js:1465 +#: htdocs/luci-static/resources/view/fchomo/node.js:1168 +#: htdocs/luci-static/resources/view/fchomo/node.js:1482 msgid "Bind interface" msgstr "綁定介面" -#: htdocs/luci-static/resources/view/fchomo/node.js:1148 -#: htdocs/luci-static/resources/view/fchomo/node.js:1466 +#: htdocs/luci-static/resources/view/fchomo/node.js:1169 +#: htdocs/luci-static/resources/view/fchomo/node.js:1483 msgid "Bind outbound interface.
" msgstr "綁定出站介面。
" @@ -316,15 +315,15 @@ msgstr "綁定出站流量至指定介面。留空自動檢測。
" msgid "Black list" msgstr "黑名單" -#: htdocs/luci-static/resources/view/fchomo/client.js:55 +#: htdocs/luci-static/resources/view/fchomo/client.js:57 msgid "Block DNS queries" msgstr "封鎖 DNS 請求" -#: htdocs/luci-static/resources/view/fchomo/client.js:1259 +#: htdocs/luci-static/resources/view/fchomo/client.js:1257 msgid "Bootstrap DNS server" msgstr "引導 DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1266 +#: htdocs/luci-static/resources/view/fchomo/client.js:1264 msgid "Bootstrap DNS server (Node)" msgstr "引導 DNS 伺服器 (節點)" @@ -332,7 +331,7 @@ msgstr "引導 DNS 伺服器 (節點)" msgid "Bypass CN" msgstr "繞過 CN 流量" -#: htdocs/luci-static/resources/view/fchomo/global.js:862 +#: htdocs/luci-static/resources/view/fchomo/global.js:877 msgid "Bypass DSCP" msgstr "繞過 DSCP" @@ -348,21 +347,21 @@ msgstr "CORS 允許私有網路" msgid "CORS allowed origins, * will be used if empty." msgstr "CORS 允許的來源,留空則使用 *。" -#: htdocs/luci-static/resources/fchomo.js:614 +#: htdocs/luci-static/resources/fchomo.js:674 msgid "Cancel" msgstr "取消" -#: htdocs/luci-static/resources/view/fchomo/node.js:869 +#: htdocs/luci-static/resources/view/fchomo/node.js:890 msgid "Cert fingerprint" msgstr "憑證指紋" -#: htdocs/luci-static/resources/view/fchomo/node.js:870 +#: htdocs/luci-static/resources/view/fchomo/node.js:891 msgid "" "Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." msgstr "憑證指紋。用於實現 SSL憑證固定 並防止 MitM。" -#: htdocs/luci-static/resources/view/fchomo/node.js:890 -#: htdocs/luci-static/resources/view/fchomo/server.js:792 +#: htdocs/luci-static/resources/view/fchomo/node.js:911 +#: htdocs/luci-static/resources/view/fchomo/server.js:778 msgid "Certificate path" msgstr "憑證路徑" @@ -391,14 +390,19 @@ msgstr "大陸 IPv6 庫版本" msgid "China list version" msgstr "大陸網域清單版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:324 -#: htdocs/luci-static/resources/view/fchomo/node.js:377 -#: htdocs/luci-static/resources/view/fchomo/node.js:565 -#: htdocs/luci-static/resources/view/fchomo/server.js:302 -#: htdocs/luci-static/resources/view/fchomo/server.js:389 +#: htdocs/luci-static/resources/view/fchomo/node.js:326 +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +#: htdocs/luci-static/resources/view/fchomo/node.js:586 +#: htdocs/luci-static/resources/view/fchomo/server.js:269 +#: htdocs/luci-static/resources/view/fchomo/server.js:356 msgid "Chipher" msgstr "加密方法" +#: htdocs/luci-static/resources/view/fchomo/node.js:388 +#: htdocs/luci-static/resources/view/fchomo/server.js:365 +msgid "Chipher must be enabled if obfuscate downlink is disabled." +msgstr "如果下行鏈路混淆功能已停用,則必須啟用加密。" + #: htdocs/luci-static/resources/view/fchomo/log.js:119 msgid "Clean log" msgstr "清空日誌" @@ -412,28 +416,28 @@ msgstr "" "最新的初始包。" #: htdocs/luci-static/resources/view/fchomo/global.js:556 -#: htdocs/luci-static/resources/view/fchomo/node.js:767 -#: htdocs/luci-static/resources/view/fchomo/node.js:891 -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:621 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/view/fchomo/node.js:788 +#: htdocs/luci-static/resources/view/fchomo/node.js:912 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 +#: htdocs/luci-static/resources/view/fchomo/server.js:607 +#: htdocs/luci-static/resources/view/fchomo/server.js:817 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 msgid "Client" msgstr "客戶端" -#: htdocs/luci-static/resources/view/fchomo/server.js:830 +#: htdocs/luci-static/resources/view/fchomo/server.js:816 msgid "Client Auth Certificate path" msgstr "客戶端認證憑證路徑" -#: htdocs/luci-static/resources/view/fchomo/server.js:822 +#: htdocs/luci-static/resources/view/fchomo/server.js:808 msgid "Client Auth type" msgstr "客戶端認證類型" -#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/node.js:952 msgid "Client fingerprint" msgstr "客戶端指紋" -#: htdocs/luci-static/resources/view/fchomo/server.js:385 +#: htdocs/luci-static/resources/view/fchomo/server.js:352 msgid "Client key" msgstr "客戶端密鑰" @@ -450,16 +454,16 @@ msgstr "收集資料中..." msgid "Common ports (bypass P2P traffic)" msgstr "常用連接埠(繞過 P2P 流量)" -#: htdocs/luci-static/resources/fchomo.js:1195 +#: htdocs/luci-static/resources/fchomo.js:1245 msgid "Complete" msgstr "完成" -#: htdocs/luci-static/resources/view/fchomo/node.js:1412 +#: htdocs/luci-static/resources/view/fchomo/node.js:1429 msgid "Configuration Items" msgstr "配置項" -#: htdocs/luci-static/resources/view/fchomo/node.js:439 -#: htdocs/luci-static/resources/view/fchomo/server.js:430 +#: htdocs/luci-static/resources/view/fchomo/node.js:460 +#: htdocs/luci-static/resources/view/fchomo/server.js:416 msgid "Congestion controller" msgstr "擁塞控制器" @@ -467,20 +471,20 @@ msgstr "擁塞控制器" msgid "Connection check" msgstr "連接檢查" -#: htdocs/luci-static/resources/view/fchomo/server.js:35 +#: htdocs/luci-static/resources/fchomo.js:543 msgid "Content copied to clipboard!" msgstr "內容已複製到剪貼簿!" -#: htdocs/luci-static/resources/view/fchomo/client.js:598 -#: htdocs/luci-static/resources/view/fchomo/node.js:1322 -#: htdocs/luci-static/resources/view/fchomo/node.js:1348 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:326 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:352 +#: htdocs/luci-static/resources/view/fchomo/client.js:608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 +#: htdocs/luci-static/resources/view/fchomo/node.js:1365 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:324 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:350 msgid "Content will not be verified, Please make sure you enter it correctly." msgstr "內容將不會被驗證,請確保輸入正確。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1321 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:325 +#: htdocs/luci-static/resources/view/fchomo/node.js:1338 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:323 msgid "Contents" msgstr "內容" @@ -488,7 +492,7 @@ msgstr "內容" msgid "Contents have been saved." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:37 +#: htdocs/luci-static/resources/fchomo.js:545 msgid "Copy" msgstr "複製" @@ -500,18 +504,23 @@ msgstr "核心版本" msgid "Cron expression" msgstr "Cron 表達式" -#: htdocs/luci-static/resources/view/fchomo/global.js:880 +#: htdocs/luci-static/resources/view/fchomo/global.js:895 msgid "Custom Direct List" msgstr "自訂直連清單" -#: htdocs/luci-static/resources/view/fchomo/node.js:1383 +#: htdocs/luci-static/resources/view/fchomo/node.js:1400 msgid "Custom HTTP header." msgstr "自訂 HTTP header。" -#: htdocs/luci-static/resources/view/fchomo/global.js:898 +#: htdocs/luci-static/resources/view/fchomo/global.js:913 msgid "Custom Proxy List" msgstr "自訂代理清單" +#: htdocs/luci-static/resources/view/fchomo/node.js:401 +#: htdocs/luci-static/resources/view/fchomo/server.js:378 +msgid "Custom byte layout" +msgstr "自訂位元組佈局" + #: htdocs/luci-static/resources/view/fchomo/hosts.js:28 msgid "" "Custom internal hosts. Support yaml or json format." @@ -521,8 +530,8 @@ msgstr "自訂內部 hosts。支援 yamljson 格 msgid "DIRECT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1542 -#: htdocs/luci-static/resources/view/fchomo/client.js:1551 +#: htdocs/luci-static/resources/view/fchomo/client.js:1536 +#: htdocs/luci-static/resources/view/fchomo/client.js:1545 msgid "DNS policy" msgstr "DNS 策略" @@ -530,18 +539,18 @@ msgstr "DNS 策略" msgid "DNS port" msgstr " DNS 連接埠" -#: htdocs/luci-static/resources/view/fchomo/client.js:1305 -#: htdocs/luci-static/resources/view/fchomo/client.js:1314 -#: htdocs/luci-static/resources/view/fchomo/client.js:1635 -#: htdocs/luci-static/resources/view/fchomo/node.js:654 +#: htdocs/luci-static/resources/view/fchomo/client.js:1303 +#: htdocs/luci-static/resources/view/fchomo/client.js:1312 +#: htdocs/luci-static/resources/view/fchomo/client.js:1625 +#: htdocs/luci-static/resources/view/fchomo/node.js:675 msgid "DNS server" msgstr "DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1245 +#: htdocs/luci-static/resources/view/fchomo/client.js:1243 msgid "DNS settings" msgstr "DNS 設定" -#: htdocs/luci-static/resources/view/fchomo/global.js:865 +#: htdocs/luci-static/resources/view/fchomo/global.js:880 msgid "DSCP list" msgstr "DSCP 清單" @@ -553,32 +562,32 @@ msgstr "面板版本" msgid "Debug" msgstr "調試" -#: htdocs/luci-static/resources/view/fchomo/client.js:53 +#: htdocs/luci-static/resources/view/fchomo/client.js:55 msgid "Default DNS (issued by WAN)" msgstr "預設 DNS(由 WAN 下發)" -#: htdocs/luci-static/resources/view/fchomo/client.js:1280 +#: htdocs/luci-static/resources/view/fchomo/client.js:1278 msgid "Default DNS server" msgstr "預設 DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/node.js:631 +#: htdocs/luci-static/resources/view/fchomo/node.js:652 msgid "Destination addresses allowed to be forwarded via Wireguard." msgstr "允許通過 WireGuard 轉發的目的位址" -#: htdocs/luci-static/resources/view/fchomo/node.js:1590 +#: htdocs/luci-static/resources/view/fchomo/node.js:1607 msgid "Destination provider" msgstr "落地供應商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1596 +#: htdocs/luci-static/resources/view/fchomo/node.js:1613 msgid "Destination proxy node" msgstr "落地代理節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:225 +#: htdocs/luci-static/resources/view/fchomo/node.js:227 msgid "Dial fields" msgstr "撥號欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:1603 -#: htdocs/luci-static/resources/view/fchomo/node.js:1623 +#: htdocs/luci-static/resources/view/fchomo/node.js:1620 +#: htdocs/luci-static/resources/view/fchomo/node.js:1640 msgid "Different chain head/tail" msgstr "不同的鏈頭/鏈尾" @@ -599,8 +608,8 @@ msgid "Direct MAC-s" msgstr "直連 MAC 位址" #: htdocs/luci-static/resources/view/fchomo/global.js:403 -#: htdocs/luci-static/resources/view/fchomo/node.js:290 -#: htdocs/luci-static/resources/view/fchomo/server.js:282 +#: htdocs/luci-static/resources/view/fchomo/node.js:292 +#: htdocs/luci-static/resources/view/fchomo/server.js:249 msgid "Disable" msgstr "停用" @@ -616,11 +625,11 @@ msgstr "停用 quic-go 的 通用分段卸載(GSO)" msgid "Disable ICMP Forwarding" msgstr "禁用 ICMP 轉發" -#: htdocs/luci-static/resources/view/fchomo/node.js:818 +#: htdocs/luci-static/resources/view/fchomo/node.js:839 msgid "Disable SNI" msgstr "停用 SNI" -#: htdocs/luci-static/resources/view/fchomo/client.js:967 +#: htdocs/luci-static/resources/view/fchomo/client.js:973 msgid "Disable UDP" msgstr "停用 UDP" @@ -628,15 +637,15 @@ msgstr "停用 UDP" msgid "Disable safe path check" msgstr "禁用安全路徑檢查" -#: htdocs/luci-static/resources/view/fchomo/client.js:1508 +#: htdocs/luci-static/resources/view/fchomo/client.js:1502 msgid "Discard A responses" msgstr "丟棄 A 回應" -#: htdocs/luci-static/resources/view/fchomo/client.js:1524 +#: htdocs/luci-static/resources/view/fchomo/client.js:1518 msgid "Discard AAAA responses" msgstr "丟棄 AAAA 回應" -#: htdocs/luci-static/resources/view/fchomo/client.js:750 +#: htdocs/luci-static/resources/view/fchomo/client.js:760 msgid "" "Do not resolve the domain connection to IP for this match.
Only works " "for pure domain inbound connections without DNS resolution. e.g., socks5h" @@ -644,37 +653,37 @@ msgstr "" "不要將網域連線解析為 IP 以進行此次匹配。
僅對未經 DNS 解析的純網域入站連" "線有效。例如,socks5h" -#: htdocs/luci-static/resources/view/fchomo/client.js:1605 -#: htdocs/luci-static/resources/view/fchomo/client.js:1610 -#: htdocs/luci-static/resources/view/fchomo/client.js:1675 -#: htdocs/luci-static/resources/view/fchomo/client.js:1682 -#: htdocs/luci-static/resources/view/fchomo/client.js:1684 +#: htdocs/luci-static/resources/view/fchomo/client.js:1595 +#: htdocs/luci-static/resources/view/fchomo/client.js:1600 +#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1672 +#: htdocs/luci-static/resources/view/fchomo/client.js:1674 msgid "Domain" msgstr "網域" -#: htdocs/luci-static/resources/view/fchomo/node.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:840 msgid "Donot send server name in ClientHello." msgstr "不要在 ClientHello 中傳送伺服器名稱。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1456 -#: htdocs/luci-static/resources/view/fchomo/node.js:883 -#: htdocs/luci-static/resources/view/fchomo/node.js:1452 +#: htdocs/luci-static/resources/view/fchomo/client.js:1450 +#: htdocs/luci-static/resources/view/fchomo/node.js:904 +#: htdocs/luci-static/resources/view/fchomo/node.js:1469 msgid "Donot verifying server certificate." msgstr "不驗證伺服器憑證。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1147 msgid "Download bandwidth" msgstr "下載頻寬" -#: htdocs/luci-static/resources/view/fchomo/node.js:1127 +#: htdocs/luci-static/resources/view/fchomo/node.js:1148 msgid "Download bandwidth in Mbps." msgstr "下載頻寬(單位:Mbps)。" -#: htdocs/luci-static/resources/fchomo.js:1080 +#: htdocs/luci-static/resources/fchomo.js:1130 msgid "Download failed: %s" msgstr "下載失敗: %s" -#: htdocs/luci-static/resources/fchomo.js:1078 +#: htdocs/luci-static/resources/fchomo.js:1128 msgid "Download successful." msgstr "下載成功。" @@ -682,20 +691,20 @@ msgstr "下載成功。" msgid "Dual stack" msgstr "雙棧" -#: htdocs/luci-static/resources/view/fchomo/node.js:924 -#: htdocs/luci-static/resources/view/fchomo/server.js:888 +#: htdocs/luci-static/resources/view/fchomo/node.js:945 +#: htdocs/luci-static/resources/view/fchomo/server.js:874 msgid "ECH config" msgstr "ECH 配置" -#: htdocs/luci-static/resources/view/fchomo/server.js:847 +#: htdocs/luci-static/resources/view/fchomo/server.js:833 msgid "ECH key" msgstr "ECH 密鑰" -#: htdocs/luci-static/resources/view/fchomo/client.js:1490 +#: htdocs/luci-static/resources/view/fchomo/client.js:1484 msgid "ECS override" msgstr "強制覆蓋 ECS" -#: htdocs/luci-static/resources/view/fchomo/client.js:1474 +#: htdocs/luci-static/resources/view/fchomo/client.js:1468 msgid "EDNS Client Subnet" msgstr "" @@ -703,24 +712,24 @@ msgstr "" msgid "ETag support" msgstr "ETag 支援" -#: htdocs/luci-static/resources/view/fchomo/node.js:1039 +#: htdocs/luci-static/resources/view/fchomo/node.js:1060 msgid "Early Data first packet length limit." msgstr "前置數據長度閾值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1045 +#: htdocs/luci-static/resources/view/fchomo/node.js:1066 msgid "Early Data header name" msgstr "前置數據標頭" -#: htdocs/luci-static/resources/view/fchomo/node.js:201 +#: htdocs/luci-static/resources/view/fchomo/node.js:203 msgid "Edit node" msgstr "編輯節點" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:124 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:126 msgid "Edit ruleset" msgstr "編輯規則集" -#: htdocs/luci-static/resources/view/fchomo/node.js:1319 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:323 +#: htdocs/luci-static/resources/view/fchomo/node.js:1336 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:321 msgid "Editer" msgstr "編輯器" @@ -728,25 +737,25 @@ msgstr "編輯器" msgid "Eliminate encryption header characteristics" msgstr "消除加密頭特徵" -#: htdocs/luci-static/resources/view/fchomo/client.js:809 -#: htdocs/luci-static/resources/view/fchomo/client.js:906 -#: htdocs/luci-static/resources/view/fchomo/client.js:1135 -#: htdocs/luci-static/resources/view/fchomo/client.js:1231 -#: htdocs/luci-static/resources/view/fchomo/client.js:1372 -#: htdocs/luci-static/resources/view/fchomo/client.js:1594 +#: htdocs/luci-static/resources/view/fchomo/client.js:819 +#: htdocs/luci-static/resources/view/fchomo/client.js:912 +#: htdocs/luci-static/resources/view/fchomo/client.js:1137 +#: htdocs/luci-static/resources/view/fchomo/client.js:1229 +#: htdocs/luci-static/resources/view/fchomo/client.js:1366 +#: htdocs/luci-static/resources/view/fchomo/client.js:1584 #: htdocs/luci-static/resources/view/fchomo/global.js:401 #: htdocs/luci-static/resources/view/fchomo/global.js:686 -#: htdocs/luci-static/resources/view/fchomo/node.js:232 -#: htdocs/luci-static/resources/view/fchomo/node.js:1292 -#: htdocs/luci-static/resources/view/fchomo/node.js:1488 -#: htdocs/luci-static/resources/view/fchomo/node.js:1577 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:251 -#: htdocs/luci-static/resources/view/fchomo/server.js:190 -#: htdocs/luci-static/resources/view/fchomo/server.js:217 +#: htdocs/luci-static/resources/view/fchomo/node.js:234 +#: htdocs/luci-static/resources/view/fchomo/node.js:1309 +#: htdocs/luci-static/resources/view/fchomo/node.js:1505 +#: htdocs/luci-static/resources/view/fchomo/node.js:1594 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:249 +#: htdocs/luci-static/resources/view/fchomo/server.js:157 +#: htdocs/luci-static/resources/view/fchomo/server.js:184 msgid "Enable" msgstr "啟用" -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:495 msgid "" "Enable 0-RTT QUIC connection handshake on the client side. This is not " "impacting much on the performance, as the protocol is fully multiplexed.
強烈建議停用此功能,因為它容易受到重放攻擊。" -#: htdocs/luci-static/resources/view/fchomo/node.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:494 msgid "Enable 0-RTT handshake" msgstr "啟用 0-RTT 握手" @@ -767,44 +776,49 @@ msgstr "" "為出站連線啟用 IP4P 轉換" -#: htdocs/luci-static/resources/view/fchomo/node.js:918 +#: htdocs/luci-static/resources/view/fchomo/node.js:939 msgid "Enable ECH" msgstr "啟用 ECH" -#: htdocs/luci-static/resources/view/fchomo/node.js:1114 +#: htdocs/luci-static/resources/view/fchomo/node.js:1135 msgid "Enable TCP Brutal" msgstr "啟用 TCP Brutal" -#: htdocs/luci-static/resources/view/fchomo/node.js:1115 +#: htdocs/luci-static/resources/view/fchomo/node.js:1136 msgid "Enable TCP Brutal congestion control algorithm" msgstr "啟用 TCP Brutal 擁塞控制演算法。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1103 +#: htdocs/luci-static/resources/view/fchomo/node.js:1124 msgid "Enable multiplexing only for TCP." msgstr "僅為 TCP 啟用多路復用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1097 +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +#: htdocs/luci-static/resources/view/fchomo/server.js:402 +msgid "Enable obfuscate for downlink" +msgstr "啟用下行鏈路混淆" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1118 msgid "Enable padding" msgstr "啟用填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1108 +#: htdocs/luci-static/resources/view/fchomo/node.js:1129 msgid "Enable statistic" msgstr "啟用統計" -#: htdocs/luci-static/resources/view/fchomo/node.js:719 -#: htdocs/luci-static/resources/view/fchomo/node.js:1434 +#: htdocs/luci-static/resources/view/fchomo/node.js:740 +#: htdocs/luci-static/resources/view/fchomo/node.js:1451 msgid "" "Enable the SUoT protocol, requires server support. Conflict with Multiplex." msgstr "啟用 SUoT 協議,需要服務端支援。與多路復用衝突。" -#: htdocs/luci-static/resources/view/fchomo/node.js:296 -#: htdocs/luci-static/resources/view/fchomo/server.js:288 +#: htdocs/luci-static/resources/view/fchomo/node.js:298 +#: htdocs/luci-static/resources/view/fchomo/server.js:255 msgid "" "Enabling obfuscation will make the server incompatible with standard QUIC " "connections, losing the ability to masquerade with HTTP/3." msgstr "啟用混淆將使伺服器與標準的 QUIC 連線不相容,失去 HTTP/3 偽裝的能力。" -#: htdocs/luci-static/resources/view/fchomo/server.js:584 +#: htdocs/luci-static/resources/view/fchomo/server.js:570 msgid "Encryption method" msgstr "加密方法" @@ -812,8 +826,8 @@ msgstr "加密方法" msgid "Endpoint-Independent NAT" msgstr "端點獨立 NAT" -#: htdocs/luci-static/resources/view/fchomo/client.js:650 -#: htdocs/luci-static/resources/view/fchomo/client.js:1627 +#: htdocs/luci-static/resources/view/fchomo/client.js:660 +#: htdocs/luci-static/resources/view/fchomo/client.js:1617 msgid "Entry" msgstr "條目" @@ -821,17 +835,17 @@ msgstr "條目" msgid "Error" msgstr "錯誤" -#: htdocs/luci-static/resources/view/fchomo/client.js:1011 +#: htdocs/luci-static/resources/view/fchomo/client.js:1017 msgid "" "Exceeding this triggers a forced health check. 5 will be used " "if empty." msgstr "超過此限制將會觸發強制健康檢查。留空則使用 5。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1546 +#: htdocs/luci-static/resources/view/fchomo/node.js:1563 msgid "Exclude matched node types." msgstr "排除匹配的節點類型。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +#: htdocs/luci-static/resources/view/fchomo/client.js:1054 msgid "" "Exclude matched node types. Available types see here." @@ -839,8 +853,8 @@ msgstr "" "排除匹配的節點類型。可用類型請參考此處。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1043 -#: htdocs/luci-static/resources/view/fchomo/node.js:1539 +#: htdocs/luci-static/resources/view/fchomo/client.js:1049 +#: htdocs/luci-static/resources/view/fchomo/node.js:1556 msgid "Exclude nodes that meet keywords or regexps." msgstr "排除匹配關鍵字或表達式的節點。" @@ -848,58 +862,60 @@ msgstr "排除匹配關鍵字或表達式的節點。" msgid "Expand/Collapse result" msgstr "展開/收起 結果" -#: htdocs/luci-static/resources/view/fchomo/client.js:1003 -#: htdocs/luci-static/resources/view/fchomo/node.js:1524 +#: htdocs/luci-static/resources/view/fchomo/client.js:1009 +#: htdocs/luci-static/resources/view/fchomo/node.js:1541 msgid "Expected HTTP code. 204 will be used if empty." msgstr "預期的 HTTP code。留空則使用 204。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1005 -#: htdocs/luci-static/resources/view/fchomo/node.js:1526 +#: htdocs/luci-static/resources/view/fchomo/client.js:1011 +#: htdocs/luci-static/resources/view/fchomo/node.js:1543 msgid "Expected status" msgstr "預期狀態" #: htdocs/luci-static/resources/fchomo.js:393 #: htdocs/luci-static/resources/fchomo.js:396 #: htdocs/luci-static/resources/fchomo.js:399 -#: htdocs/luci-static/resources/fchomo.js:1212 -#: htdocs/luci-static/resources/fchomo.js:1220 -#: htdocs/luci-static/resources/fchomo.js:1228 -#: htdocs/luci-static/resources/fchomo.js:1251 -#: htdocs/luci-static/resources/fchomo.js:1254 -#: htdocs/luci-static/resources/fchomo.js:1261 -#: htdocs/luci-static/resources/fchomo.js:1277 -#: htdocs/luci-static/resources/fchomo.js:1286 -#: htdocs/luci-static/resources/fchomo.js:1298 +#: htdocs/luci-static/resources/fchomo.js:1262 +#: htdocs/luci-static/resources/fchomo.js:1270 +#: htdocs/luci-static/resources/fchomo.js:1278 #: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1304 #: htdocs/luci-static/resources/fchomo.js:1311 -#: htdocs/luci-static/resources/fchomo.js:1323 -#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1327 #: htdocs/luci-static/resources/fchomo.js:1336 -#: htdocs/luci-static/resources/fchomo.js:1346 -#: htdocs/luci-static/resources/fchomo.js:1381 -#: htdocs/luci-static/resources/fchomo.js:1383 -#: htdocs/luci-static/resources/fchomo.js:1393 -#: htdocs/luci-static/resources/fchomo.js:1402 -#: htdocs/luci-static/resources/view/fchomo/client.js:66 -#: htdocs/luci-static/resources/view/fchomo/client.js:900 -#: htdocs/luci-static/resources/view/fchomo/client.js:1387 -#: htdocs/luci-static/resources/view/fchomo/global.js:871 -#: htdocs/luci-static/resources/view/fchomo/node.js:790 -#: htdocs/luci-static/resources/view/fchomo/node.js:875 -#: htdocs/luci-static/resources/view/fchomo/node.js:1603 -#: htdocs/luci-static/resources/view/fchomo/node.js:1623 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:278 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:292 -#: htdocs/luci-static/resources/view/fchomo/server.js:613 -#: htdocs/luci-static/resources/view/fchomo/server.js:644 -#: htdocs/luci-static/resources/view/fchomo/server.js:745 +#: htdocs/luci-static/resources/fchomo.js:1348 +#: htdocs/luci-static/resources/fchomo.js:1351 +#: htdocs/luci-static/resources/fchomo.js:1361 +#: htdocs/luci-static/resources/fchomo.js:1373 +#: htdocs/luci-static/resources/fchomo.js:1376 +#: htdocs/luci-static/resources/fchomo.js:1386 +#: htdocs/luci-static/resources/fchomo.js:1396 +#: htdocs/luci-static/resources/fchomo.js:1431 +#: htdocs/luci-static/resources/fchomo.js:1433 +#: htdocs/luci-static/resources/fchomo.js:1443 +#: htdocs/luci-static/resources/fchomo.js:1452 +#: htdocs/luci-static/resources/view/fchomo/client.js:68 +#: htdocs/luci-static/resources/view/fchomo/client.js:906 +#: htdocs/luci-static/resources/view/fchomo/client.js:1381 +#: htdocs/luci-static/resources/view/fchomo/global.js:886 +#: htdocs/luci-static/resources/view/fchomo/node.js:388 +#: htdocs/luci-static/resources/view/fchomo/node.js:811 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 +#: htdocs/luci-static/resources/view/fchomo/node.js:1620 +#: htdocs/luci-static/resources/view/fchomo/node.js:1640 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:276 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:290 +#: htdocs/luci-static/resources/view/fchomo/server.js:365 +#: htdocs/luci-static/resources/view/fchomo/server.js:599 +#: htdocs/luci-static/resources/view/fchomo/server.js:630 +#: htdocs/luci-static/resources/view/fchomo/server.js:731 msgid "Expecting: %s" msgstr "請輸入:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:980 -#: htdocs/luci-static/resources/view/fchomo/node.js:987 -#: htdocs/luci-static/resources/view/fchomo/server.js:955 -#: htdocs/luci-static/resources/view/fchomo/server.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:1001 +#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/server.js:941 +#: htdocs/luci-static/resources/view/fchomo/server.js:948 msgid "Expecting: only support %s." msgstr "請輸入:僅支援 %s." @@ -907,30 +923,30 @@ msgstr "請輸入:僅支援 %s." msgid "Experimental" msgstr "實驗性" -#: htdocs/luci-static/resources/view/fchomo/client.js:478 -#: htdocs/luci-static/resources/view/fchomo/client.js:491 -#: htdocs/luci-static/resources/view/fchomo/client.js:498 +#: htdocs/luci-static/resources/view/fchomo/client.js:488 +#: htdocs/luci-static/resources/view/fchomo/client.js:501 #: htdocs/luci-static/resources/view/fchomo/client.js:508 -#: htdocs/luci-static/resources/view/fchomo/client.js:515 -#: htdocs/luci-static/resources/view/fchomo/client.js:523 -#: htdocs/luci-static/resources/view/fchomo/client.js:530 -#: htdocs/luci-static/resources/view/fchomo/client.js:597 +#: htdocs/luci-static/resources/view/fchomo/client.js:518 +#: htdocs/luci-static/resources/view/fchomo/client.js:525 +#: htdocs/luci-static/resources/view/fchomo/client.js:533 +#: htdocs/luci-static/resources/view/fchomo/client.js:540 +#: htdocs/luci-static/resources/view/fchomo/client.js:607 msgid "Factor" msgstr "條件" -#: htdocs/luci-static/resources/fchomo.js:1153 +#: htdocs/luci-static/resources/fchomo.js:1203 msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" msgstr "無法執行 \"/etc/init.d/fchomo %s %s\" 原因: %s" -#: htdocs/luci-static/resources/fchomo.js:1112 +#: htdocs/luci-static/resources/fchomo.js:1162 msgid "Failed to generate %s, error: %s." msgstr "生成 %s 失敗,錯誤:%s。" -#: htdocs/luci-static/resources/fchomo.js:1502 +#: htdocs/luci-static/resources/fchomo.js:1552 msgid "Failed to upload %s, error: %s." msgstr "上傳 %s 失敗,錯誤:%s。" -#: htdocs/luci-static/resources/fchomo.js:1521 +#: htdocs/luci-static/resources/fchomo.js:1571 msgid "Failed to upload, error: %s." msgstr "上傳失敗,錯誤:%s。" @@ -938,42 +954,42 @@ msgstr "上傳失敗,錯誤:%s。" msgid "Fallback" msgstr "自動回退" -#: htdocs/luci-static/resources/view/fchomo/client.js:1274 -#: htdocs/luci-static/resources/view/fchomo/client.js:1287 +#: htdocs/luci-static/resources/view/fchomo/client.js:1272 +#: htdocs/luci-static/resources/view/fchomo/client.js:1285 msgid "Fallback DNS server" msgstr "後備 DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1654 +#: htdocs/luci-static/resources/view/fchomo/client.js:1644 msgid "Fallback filter" msgstr "後備過濾器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1038 -#: htdocs/luci-static/resources/view/fchomo/node.js:1533 +#: htdocs/luci-static/resources/view/fchomo/client.js:1044 +#: htdocs/luci-static/resources/view/fchomo/node.js:1550 msgid "Filter nodes that meet keywords or regexps." msgstr "過濾匹配關鍵字或表達式的節點。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1273 +#: htdocs/luci-static/resources/view/fchomo/client.js:1271 msgid "Final DNS server" msgstr "兜底 DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/client.js:1275 +#: htdocs/luci-static/resources/view/fchomo/client.js:1273 msgid "Final DNS server (For non-poisoned domains)" msgstr "兜底 DNS 伺服器 (用於未被投毒汙染的網域)" -#: htdocs/luci-static/resources/view/fchomo/client.js:1277 +#: htdocs/luci-static/resources/view/fchomo/client.js:1275 msgid "Final DNS server (For poisoned domains)" msgstr "兜底 DNS 伺服器 (用於已被投毒汙染的網域)" -#: htdocs/luci-static/resources/view/fchomo/server.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:188 msgid "Firewall" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:551 -#: htdocs/luci-static/resources/view/fchomo/server.js:494 +#: htdocs/luci-static/resources/view/fchomo/node.js:572 +#: htdocs/luci-static/resources/view/fchomo/server.js:480 msgid "Flow" msgstr "流控" -#: htdocs/luci-static/resources/view/fchomo/client.js:1027 +#: htdocs/luci-static/resources/view/fchomo/client.js:1033 msgid "" "For details, see %s." @@ -981,9 +997,9 @@ msgstr "" "實作細節請參閱 %s." -#: htdocs/luci-static/resources/view/fchomo/client.js:1004 -#: htdocs/luci-static/resources/view/fchomo/node.js:1402 -#: htdocs/luci-static/resources/view/fchomo/node.js:1525 +#: htdocs/luci-static/resources/view/fchomo/client.js:1010 +#: htdocs/luci-static/resources/view/fchomo/node.js:1419 +#: htdocs/luci-static/resources/view/fchomo/node.js:1542 msgid "" "For format see %s." @@ -991,7 +1007,7 @@ msgstr "" "格式請參閱 %s." -#: htdocs/luci-static/resources/view/fchomo/node.js:649 +#: htdocs/luci-static/resources/view/fchomo/node.js:670 msgid "Force DNS remote resolution." msgstr "強制 DNS 遠端解析。" @@ -999,7 +1015,7 @@ msgstr "強制 DNS 遠端解析。" msgid "Forced sniffing domain" msgstr "強制嗅探網域" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:283 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:281 msgid "Format" msgstr "格式" @@ -1010,7 +1026,7 @@ msgstr "格式" msgid "FullCombo Shark!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1006 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 msgid "GET" msgstr "" @@ -1022,10 +1038,10 @@ msgstr "GFW 網域清單版本" msgid "General" msgstr "常規" -#: htdocs/luci-static/resources/view/fchomo/client.js:891 -#: htdocs/luci-static/resources/view/fchomo/node.js:220 -#: htdocs/luci-static/resources/view/fchomo/node.js:1282 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 +#: htdocs/luci-static/resources/view/fchomo/client.js:897 +#: htdocs/luci-static/resources/view/fchomo/node.js:222 +#: htdocs/luci-static/resources/view/fchomo/node.js:1299 +#: htdocs/luci-static/resources/view/fchomo/server.js:171 msgid "General fields" msgstr "常規欄位" @@ -1038,9 +1054,9 @@ msgstr "常規設定" #: htdocs/luci-static/resources/fchomo.js:510 #: htdocs/luci-static/resources/fchomo.js:512 #: htdocs/luci-static/resources/view/fchomo/global.js:593 -#: htdocs/luci-static/resources/view/fchomo/server.js:376 -#: htdocs/luci-static/resources/view/fchomo/server.js:717 -#: htdocs/luci-static/resources/view/fchomo/server.js:880 +#: htdocs/luci-static/resources/view/fchomo/server.js:343 +#: htdocs/luci-static/resources/view/fchomo/server.js:703 +#: htdocs/luci-static/resources/view/fchomo/server.js:866 msgid "Generate" msgstr "生成" @@ -1056,17 +1072,17 @@ msgstr "GeoIP 版本" msgid "GeoSite version" msgstr "GeoSite 版本" -#: htdocs/luci-static/resources/view/fchomo/client.js:1664 +#: htdocs/luci-static/resources/view/fchomo/client.js:1654 msgid "Geoip code" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1661 +#: htdocs/luci-static/resources/view/fchomo/client.js:1651 msgid "Geoip enable" msgstr "Geoip 啟用" -#: htdocs/luci-static/resources/view/fchomo/client.js:1606 -#: htdocs/luci-static/resources/view/fchomo/client.js:1615 -#: htdocs/luci-static/resources/view/fchomo/client.js:1673 +#: htdocs/luci-static/resources/view/fchomo/client.js:1596 +#: htdocs/luci-static/resources/view/fchomo/client.js:1605 +#: htdocs/luci-static/resources/view/fchomo/client.js:1663 msgid "Geosite" msgstr "" @@ -1087,7 +1103,7 @@ msgstr "全域認證" msgid "Global client fingerprint" msgstr "全域客戶端指紋" -#: htdocs/luci-static/resources/view/fchomo/node.js:575 +#: htdocs/luci-static/resources/view/fchomo/node.js:596 msgid "Global padding" msgstr "全域填充" @@ -1103,107 +1119,107 @@ msgstr "Google FCM 連接埠" msgid "Grant access to fchomo configuration" msgstr "授予 fchomo 存取 UCI 配置的權限" -#: htdocs/luci-static/resources/view/fchomo/client.js:916 +#: htdocs/luci-static/resources/view/fchomo/client.js:922 msgid "Group" msgstr "組" #: htdocs/luci-static/resources/fchomo.js:126 #: htdocs/luci-static/resources/fchomo.js:158 -#: htdocs/luci-static/resources/view/fchomo/node.js:672 -#: htdocs/luci-static/resources/view/fchomo/node.js:969 -#: htdocs/luci-static/resources/view/fchomo/node.js:980 -#: htdocs/luci-static/resources/view/fchomo/server.js:955 +#: htdocs/luci-static/resources/view/fchomo/node.js:693 +#: htdocs/luci-static/resources/view/fchomo/node.js:990 +#: htdocs/luci-static/resources/view/fchomo/node.js:1001 +#: htdocs/luci-static/resources/view/fchomo/server.js:941 msgid "HTTP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:265 -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/node.js:1382 +#: htdocs/luci-static/resources/view/fchomo/node.js:267 +#: htdocs/luci-static/resources/view/fchomo/node.js:1049 +#: htdocs/luci-static/resources/view/fchomo/node.js:1399 msgid "HTTP header" msgstr "HTTP header" -#: htdocs/luci-static/resources/view/fchomo/node.js:405 +#: htdocs/luci-static/resources/view/fchomo/node.js:419 msgid "HTTP mask" msgstr "HTTP 遮罩" -#: htdocs/luci-static/resources/view/fchomo/node.js:1005 +#: htdocs/luci-static/resources/view/fchomo/node.js:1026 msgid "HTTP request method" msgstr "HTTP 請求方法" -#: htdocs/luci-static/resources/view/fchomo/client.js:1439 +#: htdocs/luci-static/resources/view/fchomo/client.js:1433 msgid "HTTP/3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:296 +#: htdocs/luci-static/resources/view/fchomo/server.js:263 msgid "" "HTTP3 server behavior when authentication fails.
A 404 page will be " "returned if empty." msgstr "身份驗證失敗時的 HTTP3 伺服器回應。預設回傳 404 頁面。" -#: htdocs/luci-static/resources/view/fchomo/node.js:970 -#: htdocs/luci-static/resources/view/fchomo/node.js:981 -#: htdocs/luci-static/resources/view/fchomo/server.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:991 +#: htdocs/luci-static/resources/view/fchomo/node.js:1002 +#: htdocs/luci-static/resources/view/fchomo/server.js:942 msgid "HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/global.js:846 +#: htdocs/luci-static/resources/view/fchomo/global.js:861 msgid "Handle domain" msgstr "處理網域" -#: htdocs/luci-static/resources/view/fchomo/node.js:363 +#: htdocs/luci-static/resources/view/fchomo/node.js:365 msgid "Handshake mode" msgstr "握手模式" -#: htdocs/luci-static/resources/view/fchomo/server.js:517 +#: htdocs/luci-static/resources/view/fchomo/server.js:503 msgid "Handshake target that supports TLS 1.3" msgstr "握手目標 (支援 TLS 1.3)" -#: htdocs/luci-static/resources/view/fchomo/server.js:417 +#: htdocs/luci-static/resources/view/fchomo/server.js:396 msgid "Handshake timeout" msgstr "握手超時" -#: htdocs/luci-static/resources/view/fchomo/client.js:973 -#: htdocs/luci-static/resources/view/fchomo/node.js:1493 +#: htdocs/luci-static/resources/view/fchomo/client.js:979 +#: htdocs/luci-static/resources/view/fchomo/node.js:1510 msgid "Health check URL" msgstr "健康檢查 URL" -#: htdocs/luci-static/resources/view/fchomo/client.js:1002 -#: htdocs/luci-static/resources/view/fchomo/node.js:1523 +#: htdocs/luci-static/resources/view/fchomo/client.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1540 msgid "Health check expected status" msgstr "健康檢查预期状态" -#: htdocs/luci-static/resources/view/fchomo/client.js:982 -#: htdocs/luci-static/resources/view/fchomo/node.js:1503 +#: htdocs/luci-static/resources/view/fchomo/client.js:988 +#: htdocs/luci-static/resources/view/fchomo/node.js:1520 msgid "Health check interval" msgstr "健康檢查间隔" -#: htdocs/luci-static/resources/view/fchomo/client.js:989 -#: htdocs/luci-static/resources/view/fchomo/node.js:1510 +#: htdocs/luci-static/resources/view/fchomo/client.js:995 +#: htdocs/luci-static/resources/view/fchomo/node.js:1527 msgid "Health check timeout" msgstr "健康檢查超时" -#: htdocs/luci-static/resources/view/fchomo/client.js:893 -#: htdocs/luci-static/resources/view/fchomo/node.js:1284 +#: htdocs/luci-static/resources/view/fchomo/client.js:899 +#: htdocs/luci-static/resources/view/fchomo/node.js:1301 msgid "Health fields" msgstr "健康欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:480 +#: htdocs/luci-static/resources/view/fchomo/node.js:501 msgid "Heartbeat interval" msgstr "心跳間隔" -#: htdocs/luci-static/resources/view/fchomo/client.js:1053 +#: htdocs/luci-static/resources/view/fchomo/client.js:1059 msgid "Hidden" msgstr "隱藏" -#: htdocs/luci-static/resources/view/fchomo/node.js:678 +#: htdocs/luci-static/resources/view/fchomo/node.js:699 msgid "Host that supports TLS 1.3" msgstr "主機名稱 (支援 TLS 1.3)" -#: htdocs/luci-static/resources/view/fchomo/node.js:318 +#: htdocs/luci-static/resources/view/fchomo/node.js:320 msgid "Host-key" msgstr "主機金鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:313 +#: htdocs/luci-static/resources/view/fchomo/node.js:315 msgid "Host-key algorithms" msgstr "主機金鑰演算法" @@ -1217,21 +1233,21 @@ msgstr "" msgid "Hysteria2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1666 -#: htdocs/luci-static/resources/view/fchomo/client.js:1679 +#: htdocs/luci-static/resources/view/fchomo/client.js:1656 +#: htdocs/luci-static/resources/view/fchomo/client.js:1669 msgid "IP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:1677 +#: htdocs/luci-static/resources/view/fchomo/client.js:1667 msgid "IP CIDR" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 msgid "IP override" msgstr "IP 覆寫" -#: htdocs/luci-static/resources/view/fchomo/node.js:1159 -#: htdocs/luci-static/resources/view/fchomo/node.js:1479 +#: htdocs/luci-static/resources/view/fchomo/node.js:1180 +#: htdocs/luci-static/resources/view/fchomo/node.js:1496 msgid "IP version" msgstr "IP 版本" @@ -1243,110 +1259,110 @@ msgstr "僅 IPv4" msgid "IPv6 only" msgstr "僅 IPv6" -#: htdocs/luci-static/resources/view/fchomo/client.js:1256 +#: htdocs/luci-static/resources/view/fchomo/client.js:1254 #: htdocs/luci-static/resources/view/fchomo/global.js:415 msgid "IPv6 support" msgstr "IPv6 支援" -#: htdocs/luci-static/resources/view/fchomo/client.js:1059 +#: htdocs/luci-static/resources/view/fchomo/client.js:1065 msgid "Icon" msgstr "圖標" -#: htdocs/luci-static/resources/view/fchomo/node.js:524 +#: htdocs/luci-static/resources/view/fchomo/node.js:545 msgid "Idle session check interval" msgstr "閒置會話檢查間隔" -#: htdocs/luci-static/resources/view/fchomo/node.js:531 +#: htdocs/luci-static/resources/view/fchomo/node.js:552 msgid "Idle session timeout" msgstr "閒置會話逾時" -#: htdocs/luci-static/resources/view/fchomo/server.js:445 +#: htdocs/luci-static/resources/view/fchomo/server.js:431 msgid "Idle timeout" msgstr "閒置逾時" -#: htdocs/luci-static/resources/fchomo.js:1254 +#: htdocs/luci-static/resources/fchomo.js:1304 msgid "If All ports is selected, uncheck others" msgstr "如果選擇了“所有連接埠”,則取消選取“其他”" -#: htdocs/luci-static/resources/view/fchomo/client.js:66 +#: htdocs/luci-static/resources/view/fchomo/client.js:68 msgid "If Block is selected, uncheck others" msgstr "如果選擇了“封鎖”,則取消選取“其他”" -#: htdocs/luci-static/resources/view/fchomo/server.js:275 +#: htdocs/luci-static/resources/view/fchomo/server.js:242 msgid "Ignore client bandwidth" msgstr "忽略客戶端頻寬" -#: htdocs/luci-static/resources/fchomo.js:619 +#: htdocs/luci-static/resources/fchomo.js:679 msgid "Import" msgstr "導入" -#: htdocs/luci-static/resources/view/fchomo/client.js:826 -#: htdocs/luci-static/resources/view/fchomo/client.js:885 -#: htdocs/luci-static/resources/view/fchomo/client.js:1082 -#: htdocs/luci-static/resources/view/fchomo/client.js:1124 -#: htdocs/luci-static/resources/view/fchomo/client.js:1192 -#: htdocs/luci-static/resources/view/fchomo/client.js:1220 -#: htdocs/luci-static/resources/view/fchomo/client.js:1321 -#: htdocs/luci-static/resources/view/fchomo/client.js:1361 -#: htdocs/luci-static/resources/view/fchomo/client.js:1558 -#: htdocs/luci-static/resources/view/fchomo/client.js:1583 -#: htdocs/luci-static/resources/view/fchomo/node.js:1184 -#: htdocs/luci-static/resources/view/fchomo/node.js:1270 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:140 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:228 +#: htdocs/luci-static/resources/view/fchomo/client.js:836 +#: htdocs/luci-static/resources/view/fchomo/client.js:891 +#: htdocs/luci-static/resources/view/fchomo/client.js:1088 +#: htdocs/luci-static/resources/view/fchomo/client.js:1126 +#: htdocs/luci-static/resources/view/fchomo/client.js:1194 +#: htdocs/luci-static/resources/view/fchomo/client.js:1218 +#: htdocs/luci-static/resources/view/fchomo/client.js:1319 +#: htdocs/luci-static/resources/view/fchomo/client.js:1355 +#: htdocs/luci-static/resources/view/fchomo/client.js:1552 +#: htdocs/luci-static/resources/view/fchomo/client.js:1573 +#: htdocs/luci-static/resources/view/fchomo/node.js:1205 +#: htdocs/luci-static/resources/view/fchomo/node.js:1287 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:142 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:226 msgid "Import mihomo config" msgstr "導入 mihomo 配置" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:181 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:179 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:230 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:234 msgid "Import rule-set links" msgstr "導入規則集連結" -#: htdocs/luci-static/resources/view/fchomo/node.js:278 -#: htdocs/luci-static/resources/view/fchomo/node.js:284 -#: htdocs/luci-static/resources/view/fchomo/node.js:1440 -#: htdocs/luci-static/resources/view/fchomo/node.js:1446 -#: htdocs/luci-static/resources/view/fchomo/server.js:264 -#: htdocs/luci-static/resources/view/fchomo/server.js:270 +#: htdocs/luci-static/resources/view/fchomo/node.js:280 +#: htdocs/luci-static/resources/view/fchomo/node.js:286 +#: htdocs/luci-static/resources/view/fchomo/node.js:1457 +#: htdocs/luci-static/resources/view/fchomo/node.js:1463 +#: htdocs/luci-static/resources/view/fchomo/server.js:231 +#: htdocs/luci-static/resources/view/fchomo/server.js:237 msgid "In Mbps." msgstr "單位為 Mbps。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1360 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:364 +#: htdocs/luci-static/resources/view/fchomo/node.js:1377 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:362 msgid "In bytes. %s will be used if empty." msgstr "單位為位元組。留空則使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:481 -#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:509 msgid "In millisecond." msgstr "單位為毫秒。" -#: htdocs/luci-static/resources/view/fchomo/client.js:990 -#: htdocs/luci-static/resources/view/fchomo/client.js:1019 -#: htdocs/luci-static/resources/view/fchomo/node.js:1511 +#: htdocs/luci-static/resources/view/fchomo/client.js:996 +#: htdocs/luci-static/resources/view/fchomo/client.js:1025 +#: htdocs/luci-static/resources/view/fchomo/node.js:1528 msgid "In millisecond. %s will be used if empty." msgstr "單位為毫秒。留空則使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:525 -#: htdocs/luci-static/resources/view/fchomo/node.js:532 -#: htdocs/luci-static/resources/view/fchomo/server.js:446 -#: htdocs/luci-static/resources/view/fchomo/server.js:453 +#: htdocs/luci-static/resources/view/fchomo/node.js:546 +#: htdocs/luci-static/resources/view/fchomo/node.js:553 +#: htdocs/luci-static/resources/view/fchomo/server.js:432 +#: htdocs/luci-static/resources/view/fchomo/server.js:439 msgid "In seconds." msgstr "單位為秒。" -#: htdocs/luci-static/resources/view/fchomo/client.js:983 +#: htdocs/luci-static/resources/view/fchomo/client.js:989 #: htdocs/luci-static/resources/view/fchomo/global.js:425 #: htdocs/luci-static/resources/view/fchomo/global.js:430 #: htdocs/luci-static/resources/view/fchomo/global.js:515 -#: htdocs/luci-static/resources/view/fchomo/node.js:1366 -#: htdocs/luci-static/resources/view/fchomo/node.js:1504 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:370 +#: htdocs/luci-static/resources/view/fchomo/node.js:1383 +#: htdocs/luci-static/resources/view/fchomo/node.js:1521 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:368 msgid "In seconds. %s will be used if empty." msgstr "單位為秒。留空則使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:779 -#: htdocs/luci-static/resources/view/fchomo/server.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:800 +#: htdocs/luci-static/resources/view/fchomo/server.js:619 msgid "" "In the order of one Padding-Length and one Padding-" "Interval, infinite concatenation." @@ -1358,27 +1374,27 @@ msgstr "" msgid "Inbound" msgstr "入站" -#: htdocs/luci-static/resources/view/fchomo/client.js:951 +#: htdocs/luci-static/resources/view/fchomo/client.js:957 msgid "Include all" msgstr "引入所有" -#: htdocs/luci-static/resources/view/fchomo/client.js:956 +#: htdocs/luci-static/resources/view/fchomo/client.js:962 msgid "Include all node" msgstr "引入所有節點" -#: htdocs/luci-static/resources/view/fchomo/client.js:961 +#: htdocs/luci-static/resources/view/fchomo/client.js:967 msgid "Include all provider" msgstr "引入所有供應商" -#: htdocs/luci-static/resources/view/fchomo/client.js:962 +#: htdocs/luci-static/resources/view/fchomo/client.js:968 msgid "Includes all Provider." msgstr "引入所有供應商。" -#: htdocs/luci-static/resources/view/fchomo/client.js:952 +#: htdocs/luci-static/resources/view/fchomo/client.js:958 msgid "Includes all Proxy Node and Provider." msgstr "引入所有代理節點及供應商。" -#: htdocs/luci-static/resources/view/fchomo/client.js:957 +#: htdocs/luci-static/resources/view/fchomo/client.js:963 msgid "Includes all Proxy Node." msgstr "引入所有代理節點。" @@ -1386,8 +1402,8 @@ msgstr "引入所有代理節點。" msgid "Info" msgstr "訊息" -#: htdocs/luci-static/resources/view/fchomo/node.js:1299 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:266 +#: htdocs/luci-static/resources/view/fchomo/node.js:1316 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 msgid "Inline" msgstr "內嵌" @@ -1399,39 +1415,39 @@ msgstr "介面控制" msgid "Keep default" msgstr "保持預設" -#: htdocs/luci-static/resources/view/fchomo/node.js:371 -#: htdocs/luci-static/resources/view/fchomo/server.js:332 +#: htdocs/luci-static/resources/view/fchomo/node.js:373 +#: htdocs/luci-static/resources/view/fchomo/server.js:299 msgid "Key" msgstr "密鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:904 -#: htdocs/luci-static/resources/view/fchomo/server.js:807 +#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/server.js:793 msgid "Key path" msgstr "憑證路徑" -#: htdocs/luci-static/resources/view/fchomo/server.js:652 +#: htdocs/luci-static/resources/view/fchomo/server.js:638 msgid "Keypairs" msgstr "密鑰對" -#: htdocs/luci-static/resources/view/fchomo/client.js:896 -#: htdocs/luci-static/resources/view/fchomo/client.js:1130 -#: htdocs/luci-static/resources/view/fchomo/client.js:1226 -#: htdocs/luci-static/resources/view/fchomo/client.js:1367 -#: htdocs/luci-static/resources/view/fchomo/client.js:1589 -#: htdocs/luci-static/resources/view/fchomo/node.js:227 -#: htdocs/luci-static/resources/view/fchomo/node.js:1287 -#: htdocs/luci-static/resources/view/fchomo/node.js:1572 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 -#: htdocs/luci-static/resources/view/fchomo/server.js:212 +#: htdocs/luci-static/resources/view/fchomo/client.js:902 +#: htdocs/luci-static/resources/view/fchomo/client.js:1132 +#: htdocs/luci-static/resources/view/fchomo/client.js:1224 +#: htdocs/luci-static/resources/view/fchomo/client.js:1361 +#: htdocs/luci-static/resources/view/fchomo/client.js:1579 +#: htdocs/luci-static/resources/view/fchomo/node.js:229 +#: htdocs/luci-static/resources/view/fchomo/node.js:1304 +#: htdocs/luci-static/resources/view/fchomo/node.js:1589 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:244 +#: htdocs/luci-static/resources/view/fchomo/server.js:179 msgid "Label" msgstr "標籤" -#: htdocs/luci-static/resources/view/fchomo/client.js:996 -#: htdocs/luci-static/resources/view/fchomo/node.js:1517 +#: htdocs/luci-static/resources/view/fchomo/client.js:1002 +#: htdocs/luci-static/resources/view/fchomo/node.js:1534 msgid "Lazy" msgstr "懶惰狀態" -#: htdocs/luci-static/resources/view/fchomo/server.js:503 +#: htdocs/luci-static/resources/view/fchomo/server.js:489 msgid "" "Legacy protocol support (VMess MD5 Authentication) is provided for " "compatibility purposes only, use of alterId > 1 is not recommended." @@ -1443,16 +1459,16 @@ msgstr "" msgid "Less compatibility and sometimes better performance." msgstr "有時效能較好。" -#: htdocs/luci-static/resources/view/fchomo/node.js:831 -#: htdocs/luci-static/resources/view/fchomo/server.js:788 +#: htdocs/luci-static/resources/view/fchomo/node.js:852 +#: htdocs/luci-static/resources/view/fchomo/server.js:774 msgid "List of supported application level protocols, in order of preference." msgstr "支援的應用層協議協商清單,依序排列。" -#: htdocs/luci-static/resources/view/fchomo/server.js:232 +#: htdocs/luci-static/resources/view/fchomo/server.js:199 msgid "Listen address" msgstr "監聽位址" -#: htdocs/luci-static/resources/view/fchomo/server.js:209 +#: htdocs/luci-static/resources/view/fchomo/server.js:176 msgid "Listen fields" msgstr "監聽欄位" @@ -1460,8 +1476,8 @@ msgstr "監聽欄位" msgid "Listen interfaces" msgstr "監聽介面" -#: htdocs/luci-static/resources/view/fchomo/client.js:1251 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/client.js:1249 +#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Listen port" msgstr "監聽埠" @@ -1473,16 +1489,16 @@ msgstr "監聽埠" msgid "Load balance" msgstr "負載均衡" -#: htdocs/luci-static/resources/view/fchomo/node.js:1297 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 +#: htdocs/luci-static/resources/view/fchomo/node.js:1314 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:262 msgid "Local" msgstr "本地" -#: htdocs/luci-static/resources/view/fchomo/node.js:602 +#: htdocs/luci-static/resources/view/fchomo/node.js:623 msgid "Local IPv6 address" msgstr "本地 IPv6 位址" -#: htdocs/luci-static/resources/view/fchomo/node.js:595 +#: htdocs/luci-static/resources/view/fchomo/node.js:616 msgid "Local address" msgstr "本地位址" @@ -1507,75 +1523,75 @@ msgid "Lowercase only" msgstr "僅限小寫" #: htdocs/luci-static/resources/view/fchomo/global.js:502 -#: htdocs/luci-static/resources/view/fchomo/node.js:642 +#: htdocs/luci-static/resources/view/fchomo/node.js:663 msgid "MTU" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:295 +#: htdocs/luci-static/resources/view/fchomo/server.js:262 msgid "Masquerade" msgstr "偽裝" -#: htdocs/luci-static/resources/view/fchomo/client.js:1611 +#: htdocs/luci-static/resources/view/fchomo/client.js:1601 msgid "Match domain. Support wildcards." msgstr "匹配網域。支援通配符。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1683 +#: htdocs/luci-static/resources/view/fchomo/client.js:1673 msgid "Match domain. Support wildcards.
" msgstr "匹配網域。支援通配符。
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1616 +#: htdocs/luci-static/resources/view/fchomo/client.js:1606 msgid "Match geosite." msgstr "匹配 geosite。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1674 +#: htdocs/luci-static/resources/view/fchomo/client.js:1664 msgid "Match geosite.
" msgstr "匹配 geosite。
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1655 msgid "Match response with geoip.
" msgstr "匹配回應通過 geoip。
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1678 +#: htdocs/luci-static/resources/view/fchomo/client.js:1668 msgid "Match response with ipcidr.
" msgstr "匹配回應通過 ipcidr
" -#: htdocs/luci-static/resources/view/fchomo/client.js:1621 +#: htdocs/luci-static/resources/view/fchomo/client.js:1611 msgid "Match rule set." msgstr "匹配規則集。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1038 +#: htdocs/luci-static/resources/view/fchomo/node.js:1059 msgid "Max Early Data" msgstr "前置數據最大值" -#: htdocs/luci-static/resources/view/fchomo/node.js:467 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/server.js:425 msgid "Max UDP relay packet size" msgstr "UDP 中繼數據包最大尺寸" -#: htdocs/luci-static/resources/view/fchomo/client.js:1010 +#: htdocs/luci-static/resources/view/fchomo/client.js:1016 msgid "Max count of failures" msgstr "最大失敗次數" -#: htdocs/luci-static/resources/view/fchomo/node.js:283 -#: htdocs/luci-static/resources/view/fchomo/server.js:269 +#: htdocs/luci-static/resources/view/fchomo/node.js:285 +#: htdocs/luci-static/resources/view/fchomo/server.js:236 msgid "Max download speed" msgstr "最大下載速度" -#: htdocs/luci-static/resources/view/fchomo/node.js:494 +#: htdocs/luci-static/resources/view/fchomo/node.js:515 msgid "Max open streams" msgstr "限制打開流的數量" -#: htdocs/luci-static/resources/view/fchomo/node.js:277 -#: htdocs/luci-static/resources/view/fchomo/server.js:263 +#: htdocs/luci-static/resources/view/fchomo/node.js:279 +#: htdocs/luci-static/resources/view/fchomo/server.js:230 msgid "Max upload speed" msgstr "最大上傳速度" -#: htdocs/luci-static/resources/view/fchomo/node.js:1075 -#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/node.js:1096 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "Maximum connections" msgstr "最大連線數" -#: htdocs/luci-static/resources/view/fchomo/node.js:1089 +#: htdocs/luci-static/resources/view/fchomo/node.js:1110 msgid "" "Maximum multiplexed streams in a connection before opening a new connection." "
Conflict with %s and %s." @@ -1583,12 +1599,12 @@ msgstr "" "在開啟新連線之前,連線中的最大多路復用流數量。
%s 和 " "%s 衝突。" -#: htdocs/luci-static/resources/view/fchomo/node.js:398 -#: htdocs/luci-static/resources/view/fchomo/server.js:410 +#: htdocs/luci-static/resources/view/fchomo/node.js:412 +#: htdocs/luci-static/resources/view/fchomo/server.js:389 msgid "Maximum padding" msgstr "最大填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1088 +#: htdocs/luci-static/resources/view/fchomo/node.js:1109 msgid "Maximum streams" msgstr "最大流數量" @@ -1597,7 +1613,7 @@ msgstr "最大流數量" msgid "Mieru" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:781 +#: htdocs/luci-static/resources/view/fchomo/client.js:791 #: htdocs/luci-static/resources/view/fchomo/log.js:149 #: htdocs/luci-static/resources/view/fchomo/log.js:154 msgid "Mihomo client" @@ -1605,26 +1621,26 @@ msgstr "Mihomo 客戶端" #: htdocs/luci-static/resources/view/fchomo/log.js:158 #: htdocs/luci-static/resources/view/fchomo/log.js:163 -#: htdocs/luci-static/resources/view/fchomo/server.js:164 +#: htdocs/luci-static/resources/view/fchomo/server.js:131 msgid "Mihomo server" msgstr "Mihomo 服務端" -#: htdocs/luci-static/resources/view/fchomo/node.js:538 +#: htdocs/luci-static/resources/view/fchomo/node.js:559 msgid "Min of idle sessions to keep" msgstr "要保留的最少閒置會話數" -#: htdocs/luci-static/resources/view/fchomo/node.js:1082 +#: htdocs/luci-static/resources/view/fchomo/node.js:1103 msgid "" "Minimum multiplexed streams in a connection before opening a new connection." msgstr "在開啟新連線之前,連線中的最小多路復用流數量。" -#: htdocs/luci-static/resources/view/fchomo/node.js:391 -#: htdocs/luci-static/resources/view/fchomo/server.js:403 +#: htdocs/luci-static/resources/view/fchomo/node.js:405 +#: htdocs/luci-static/resources/view/fchomo/server.js:382 msgid "Minimum padding" msgstr "最小填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1081 -#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/node.js:1102 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "Minimum streams" msgstr "最小流數量" @@ -1641,33 +1657,33 @@ msgstr "混合 系統 TCP 堆栈和 gVisor UDP 堆栈 msgid "Mixed port" msgstr "混合連接埠" -#: htdocs/luci-static/resources/view/fchomo/node.js:1061 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 msgid "Multiplex" msgstr "多路復用" -#: htdocs/luci-static/resources/view/fchomo/node.js:224 -#: htdocs/luci-static/resources/view/fchomo/server.js:208 +#: htdocs/luci-static/resources/view/fchomo/node.js:226 +#: htdocs/luci-static/resources/view/fchomo/server.js:175 msgid "Multiplex fields" msgstr "多路復用欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:354 +#: htdocs/luci-static/resources/view/fchomo/node.js:356 msgid "Multiplexing" msgstr "多路復用" -#: htdocs/luci-static/resources/view/fchomo/client.js:541 -#: htdocs/luci-static/resources/view/fchomo/client.js:616 +#: htdocs/luci-static/resources/view/fchomo/client.js:551 +#: htdocs/luci-static/resources/view/fchomo/client.js:626 msgid "NOT" msgstr "NOT" -#: htdocs/luci-static/resources/view/fchomo/node.js:1372 +#: htdocs/luci-static/resources/view/fchomo/node.js:1389 msgid "Name of the Proxy group to download provider." msgstr "用於下載供應商訂閱的代理組名稱。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:376 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:374 msgid "Name of the Proxy group to download rule set." msgstr "用於下載規則集訂閱的代理組名稱。" -#: htdocs/luci-static/resources/view/fchomo/node.js:451 +#: htdocs/luci-static/resources/view/fchomo/node.js:472 msgid "Native UDP" msgstr "原生 UDP" @@ -1679,45 +1695,45 @@ msgstr "原生外觀" msgid "No Authentication IP ranges" msgstr "無需認證的 IP 範圍" -#: htdocs/luci-static/resources/view/fchomo/client.js:1387 +#: htdocs/luci-static/resources/view/fchomo/client.js:1381 msgid "No add'l params" msgstr "無附加參數" -#: htdocs/luci-static/resources/view/fchomo/client.js:997 -#: htdocs/luci-static/resources/view/fchomo/node.js:1518 +#: htdocs/luci-static/resources/view/fchomo/client.js:1003 +#: htdocs/luci-static/resources/view/fchomo/node.js:1535 msgid "No testing is performed when this provider node is not in use." msgstr "當此供應商的節點未使用時,不執行任何測試。" -#: htdocs/luci-static/resources/fchomo.js:570 +#: htdocs/luci-static/resources/fchomo.js:640 msgid "No valid %s found." msgstr "未找到有效的%s。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:207 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:205 msgid "No valid rule-set link found." msgstr "未找到有效的規則集連結。" -#: htdocs/luci-static/resources/view/fchomo/client.js:923 -#: htdocs/luci-static/resources/view/fchomo/node.js:215 +#: htdocs/luci-static/resources/view/fchomo/client.js:929 +#: htdocs/luci-static/resources/view/fchomo/node.js:217 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 msgid "Node" msgstr "節點" -#: htdocs/luci-static/resources/view/fchomo/client.js:1042 -#: htdocs/luci-static/resources/view/fchomo/node.js:1538 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +#: htdocs/luci-static/resources/view/fchomo/node.js:1555 msgid "Node exclude filter" msgstr "排除節點" -#: htdocs/luci-static/resources/view/fchomo/client.js:1047 -#: htdocs/luci-static/resources/view/fchomo/node.js:1545 +#: htdocs/luci-static/resources/view/fchomo/client.js:1053 +#: htdocs/luci-static/resources/view/fchomo/node.js:1562 msgid "Node exclude type" msgstr "排除節點類型" -#: htdocs/luci-static/resources/view/fchomo/client.js:1037 -#: htdocs/luci-static/resources/view/fchomo/node.js:1532 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +#: htdocs/luci-static/resources/view/fchomo/node.js:1549 msgid "Node filter" msgstr "過濾節點" -#: htdocs/luci-static/resources/view/fchomo/client.js:1018 +#: htdocs/luci-static/resources/view/fchomo/client.js:1024 msgid "Node switch tolerance" msgstr "節點切換容差" @@ -1729,37 +1745,37 @@ msgstr "無" msgid "Not Installed" msgstr "未安裝" -#: htdocs/luci-static/resources/fchomo.js:1038 +#: htdocs/luci-static/resources/fchomo.js:1088 msgid "Not Running" msgstr "未在運作" -#: htdocs/luci-static/resources/view/fchomo/node.js:671 +#: htdocs/luci-static/resources/view/fchomo/node.js:692 msgid "Obfs Mode" msgstr "Obfs 模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:295 -#: htdocs/luci-static/resources/view/fchomo/server.js:287 +#: htdocs/luci-static/resources/view/fchomo/node.js:297 +#: htdocs/luci-static/resources/view/fchomo/server.js:254 msgid "Obfuscate password" msgstr "混淆密碼" -#: htdocs/luci-static/resources/view/fchomo/node.js:289 -#: htdocs/luci-static/resources/view/fchomo/node.js:385 -#: htdocs/luci-static/resources/view/fchomo/server.js:281 -#: htdocs/luci-static/resources/view/fchomo/server.js:397 +#: htdocs/luci-static/resources/view/fchomo/node.js:291 +#: htdocs/luci-static/resources/view/fchomo/node.js:395 +#: htdocs/luci-static/resources/view/fchomo/server.js:248 +#: htdocs/luci-static/resources/view/fchomo/server.js:372 msgid "Obfuscate type" msgstr "混淆類型" -#: htdocs/luci-static/resources/view/fchomo/node.js:386 -#: htdocs/luci-static/resources/view/fchomo/server.js:398 +#: htdocs/luci-static/resources/view/fchomo/node.js:396 +#: htdocs/luci-static/resources/view/fchomo/server.js:373 msgid "Obfuscated as ASCII data stream" msgstr "混淆為 ASCII 資料流" -#: htdocs/luci-static/resources/view/fchomo/node.js:387 -#: htdocs/luci-static/resources/view/fchomo/server.js:399 +#: htdocs/luci-static/resources/view/fchomo/node.js:397 +#: htdocs/luci-static/resources/view/fchomo/server.js:374 msgid "Obfuscated as low-entropy data stream" msgstr "混淆為低熵資料流" -#: htdocs/luci-static/resources/view/fchomo/global.js:871 +#: htdocs/luci-static/resources/view/fchomo/global.js:886 msgid "One or more numbers in the range 0-63 separated by commas" msgstr "0-63 範圍內的一個或多個數字,以逗號分隔" @@ -1767,7 +1783,7 @@ msgstr "0-63 範圍內的一個或多個數字,以逗號分隔" msgid "Only process traffic from specific interfaces. Leave empty for all." msgstr "只處理來自指定介面的流量。留空表示全部。" -#: htdocs/luci-static/resources/fchomo.js:1032 +#: htdocs/luci-static/resources/fchomo.js:1082 msgid "Open Dashboard" msgstr "打開面板" @@ -1780,16 +1796,16 @@ msgstr "運作模式" msgid "Override destination" msgstr "覆蓋目標位址" -#: htdocs/luci-static/resources/view/fchomo/client.js:892 -#: htdocs/luci-static/resources/view/fchomo/node.js:1283 +#: htdocs/luci-static/resources/view/fchomo/client.js:898 +#: htdocs/luci-static/resources/view/fchomo/node.js:1300 msgid "Override fields" msgstr "覆蓋欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:434 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 msgid "Override the IP address of the server that DNS response." msgstr "覆蓋 DNS 回應的伺服器的 IP 位址。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1644 +#: htdocs/luci-static/resources/view/fchomo/client.js:1634 msgid "Override the Proxy group of DNS server." msgstr "覆蓋 DNS 伺服器所使用的代理組。" @@ -1797,7 +1813,7 @@ msgstr "覆蓋 DNS 伺服器所使用的代理組。" msgid "Override the connection destination address with the sniffed domain." msgstr "使用嗅探到的網域覆寫連線目標。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1491 +#: htdocs/luci-static/resources/view/fchomo/client.js:1485 msgid "Override the existing ECS in original request." msgstr "覆蓋原始請求中已有的 ECS。" @@ -1805,43 +1821,43 @@ msgstr "覆蓋原始請求中已有的 ECS。" msgid "Overview" msgstr "概覽" -#: htdocs/luci-static/resources/view/fchomo/node.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:1028 msgid "POST" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1029 msgid "PUT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:587 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 msgid "Packet encoding" msgstr "數據包編碼" -#: htdocs/luci-static/resources/view/fchomo/server.js:483 +#: htdocs/luci-static/resources/view/fchomo/server.js:469 msgid "Padding scheme" msgstr "填充方案" -#: htdocs/luci-static/resources/view/fchomo/node.js:777 -#: htdocs/luci-static/resources/view/fchomo/server.js:631 +#: htdocs/luci-static/resources/view/fchomo/node.js:798 +#: htdocs/luci-static/resources/view/fchomo/server.js:617 msgid "Paddings" msgstr "填充 (Paddings)" -#: htdocs/luci-static/resources/view/fchomo/node.js:259 -#: htdocs/luci-static/resources/view/fchomo/node.js:332 -#: htdocs/luci-static/resources/view/fchomo/node.js:686 -#: htdocs/luci-static/resources/view/fchomo/server.js:254 -#: htdocs/luci-static/resources/view/fchomo/server.js:310 -#: htdocs/luci-static/resources/view/fchomo/server.js:524 +#: htdocs/luci-static/resources/view/fchomo/node.js:261 +#: htdocs/luci-static/resources/view/fchomo/node.js:334 +#: htdocs/luci-static/resources/view/fchomo/node.js:707 +#: htdocs/luci-static/resources/view/fchomo/server.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:277 +#: htdocs/luci-static/resources/view/fchomo/server.js:510 msgid "Password" msgstr "密碼" -#: htdocs/luci-static/resources/view/fchomo/node.js:1347 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:351 -#: htdocs/luci-static/resources/view/fchomo/server.js:571 +#: htdocs/luci-static/resources/view/fchomo/node.js:1364 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:349 +#: htdocs/luci-static/resources/view/fchomo/server.js:557 msgid "Payload" msgstr "Payload" -#: htdocs/luci-static/resources/view/fchomo/node.js:616 +#: htdocs/luci-static/resources/view/fchomo/node.js:637 msgid "Peer pubkic key" msgstr "對端公鑰" @@ -1851,11 +1867,11 @@ msgid "" "it is not needed." msgstr "效能可能會略有下降,建議僅在需要時開啟。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:282 msgid "Plain text" msgstr "純文本 text" -#: htdocs/luci-static/resources/view/fchomo/global.js:848 +#: htdocs/luci-static/resources/view/fchomo/global.js:863 msgid "" "Please ensure that the DNS query of the domains to be processed in the DNS " "policy
are send via DIRECT/Proxy Node in the same semantics as Routing " @@ -1864,58 +1880,58 @@ msgstr "" "請在 DNS 策略 中確保要處理的網域的 DNS 查詢
以與路由模式相同的語意透過 直" "連/代理節點 傳送。" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:184 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:182 msgid "" "Please refer to
%s for link format " "standards." msgstr "連結格式標準請參考 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1320 -#: htdocs/luci-static/resources/view/fchomo/node.js:1346 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:324 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:350 +#: htdocs/luci-static/resources/view/fchomo/node.js:1337 +#: htdocs/luci-static/resources/view/fchomo/node.js:1363 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:322 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:348 msgid "" "Please type %s." msgstr "" "請輸入 %s。" -#: htdocs/luci-static/resources/view/fchomo/client.js:827 -#: htdocs/luci-static/resources/view/fchomo/client.js:1083 -#: htdocs/luci-static/resources/view/fchomo/client.js:1193 -#: htdocs/luci-static/resources/view/fchomo/client.js:1322 -#: htdocs/luci-static/resources/view/fchomo/client.js:1559 -#: htdocs/luci-static/resources/view/fchomo/node.js:1185 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:141 +#: htdocs/luci-static/resources/view/fchomo/client.js:837 +#: htdocs/luci-static/resources/view/fchomo/client.js:1089 +#: htdocs/luci-static/resources/view/fchomo/client.js:1195 +#: htdocs/luci-static/resources/view/fchomo/client.js:1320 +#: htdocs/luci-static/resources/view/fchomo/client.js:1553 +#: htdocs/luci-static/resources/view/fchomo/node.js:1206 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:143 msgid "Please type %s fields of mihomo config.
" msgstr "請輸入 mihomo 配置的 %s 欄位。
" -#: htdocs/luci-static/resources/view/fchomo/node.js:660 -#: htdocs/luci-static/resources/view/fchomo/server.js:510 +#: htdocs/luci-static/resources/view/fchomo/node.js:681 +#: htdocs/luci-static/resources/view/fchomo/server.js:496 msgid "Plugin" msgstr "插件" -#: htdocs/luci-static/resources/view/fchomo/node.js:671 -#: htdocs/luci-static/resources/view/fchomo/node.js:678 -#: htdocs/luci-static/resources/view/fchomo/node.js:686 #: htdocs/luci-static/resources/view/fchomo/node.js:692 -#: htdocs/luci-static/resources/view/fchomo/node.js:700 -#: htdocs/luci-static/resources/view/fchomo/node.js:706 -#: htdocs/luci-static/resources/view/fchomo/server.js:517 -#: htdocs/luci-static/resources/view/fchomo/server.js:524 -#: htdocs/luci-static/resources/view/fchomo/server.js:530 +#: htdocs/luci-static/resources/view/fchomo/node.js:699 +#: htdocs/luci-static/resources/view/fchomo/node.js:707 +#: htdocs/luci-static/resources/view/fchomo/node.js:713 +#: htdocs/luci-static/resources/view/fchomo/node.js:721 +#: htdocs/luci-static/resources/view/fchomo/node.js:727 +#: htdocs/luci-static/resources/view/fchomo/server.js:503 +#: htdocs/luci-static/resources/view/fchomo/server.js:510 +#: htdocs/luci-static/resources/view/fchomo/server.js:516 msgid "Plugin:" msgstr "插件:" -#: htdocs/luci-static/resources/view/fchomo/node.js:247 +#: htdocs/luci-static/resources/view/fchomo/node.js:249 msgid "Port" msgstr "連接埠" -#: htdocs/luci-static/resources/fchomo.js:1263 +#: htdocs/luci-static/resources/fchomo.js:1313 msgid "Port %s alrealy exists!" msgstr "連接埠 %s 已存在!" -#: htdocs/luci-static/resources/view/fchomo/node.js:342 +#: htdocs/luci-static/resources/view/fchomo/node.js:344 msgid "Port range" msgstr "連接埠範圍" @@ -1923,13 +1939,13 @@ msgstr "連接埠範圍" msgid "Ports" msgstr "連接埠" -#: htdocs/luci-static/resources/view/fchomo/node.js:272 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/node.js:274 +#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Ports pool" msgstr "連接埠池" -#: htdocs/luci-static/resources/view/fchomo/node.js:411 -#: htdocs/luci-static/resources/view/fchomo/node.js:623 +#: htdocs/luci-static/resources/view/fchomo/node.js:432 +#: htdocs/luci-static/resources/view/fchomo/node.js:644 msgid "Pre-shared key" msgstr "預先共用金鑰" @@ -1948,22 +1964,22 @@ msgstr "防止某些情況下的 ICMP 環回問題。 Ping 不會顯示實際延 #: htdocs/luci-static/resources/view/fchomo/global.js:742 #: htdocs/luci-static/resources/view/fchomo/global.js:759 -#: htdocs/luci-static/resources/view/fchomo/node.js:1149 -#: htdocs/luci-static/resources/view/fchomo/node.js:1155 -#: htdocs/luci-static/resources/view/fchomo/node.js:1467 -#: htdocs/luci-static/resources/view/fchomo/node.js:1474 +#: htdocs/luci-static/resources/view/fchomo/node.js:1170 +#: htdocs/luci-static/resources/view/fchomo/node.js:1176 +#: htdocs/luci-static/resources/view/fchomo/node.js:1484 +#: htdocs/luci-static/resources/view/fchomo/node.js:1491 msgid "Priority: Proxy Node > Global." msgstr "優先權: 代理節點 > 全域。" -#: htdocs/luci-static/resources/view/fchomo/node.js:304 +#: htdocs/luci-static/resources/view/fchomo/node.js:306 msgid "Priv-key" msgstr "金鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:308 +#: htdocs/luci-static/resources/view/fchomo/node.js:310 msgid "Priv-key passphrase" msgstr "金鑰密碼" -#: htdocs/luci-static/resources/view/fchomo/node.js:608 +#: htdocs/luci-static/resources/view/fchomo/node.js:629 msgid "Private key" msgstr "私鑰" @@ -1972,34 +1988,34 @@ msgid "Process matching mode" msgstr "進程匹配模式" #: htdocs/luci-static/resources/view/fchomo/global.js:690 -#: htdocs/luci-static/resources/view/fchomo/node.js:1067 +#: htdocs/luci-static/resources/view/fchomo/node.js:1088 msgid "Protocol" msgstr "協議" -#: htdocs/luci-static/resources/view/fchomo/node.js:582 +#: htdocs/luci-static/resources/view/fchomo/node.js:603 msgid "Protocol parameter. Enable length block encryption." msgstr "協議參數。啟用長度塊加密。" -#: htdocs/luci-static/resources/view/fchomo/node.js:576 +#: htdocs/luci-static/resources/view/fchomo/node.js:597 msgid "" "Protocol parameter. Will waste traffic randomly if enabled (enabled by " "default in v2ray and cannot be disabled)." msgstr "協議參數。 如啟用會隨機浪費流量(在 v2ray 中預設為啟用且無法停用)。" -#: htdocs/luci-static/resources/view/fchomo/client.js:937 -#: htdocs/luci-static/resources/view/fchomo/node.js:1168 -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 -#: htdocs/luci-static/resources/view/fchomo/node.js:1583 +#: htdocs/luci-static/resources/view/fchomo/client.js:943 +#: htdocs/luci-static/resources/view/fchomo/node.js:1189 +#: htdocs/luci-static/resources/view/fchomo/node.js:1198 +#: htdocs/luci-static/resources/view/fchomo/node.js:1600 msgid "Provider" msgstr "供應商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1353 +#: htdocs/luci-static/resources/view/fchomo/node.js:1370 msgid "Provider URL" msgstr "供應商訂閱 URL" -#: htdocs/luci-static/resources/view/fchomo/client.js:801 -#: htdocs/luci-static/resources/view/fchomo/client.js:819 -#: htdocs/luci-static/resources/view/fchomo/client.js:1278 +#: htdocs/luci-static/resources/view/fchomo/client.js:811 +#: htdocs/luci-static/resources/view/fchomo/client.js:829 +#: htdocs/luci-static/resources/view/fchomo/client.js:1276 msgid "Proxy Group" msgstr "代理組" @@ -2015,24 +2031,24 @@ msgstr "代理 IPv6 位址" msgid "Proxy MAC-s" msgstr "代理 MAC 位址" -#: htdocs/luci-static/resources/view/fchomo/node.js:206 -#: htdocs/luci-static/resources/view/fchomo/node.js:1582 +#: htdocs/luci-static/resources/view/fchomo/node.js:208 +#: htdocs/luci-static/resources/view/fchomo/node.js:1599 msgid "Proxy Node" msgstr "代理節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:1558 -#: htdocs/luci-static/resources/view/fchomo/node.js:1567 +#: htdocs/luci-static/resources/view/fchomo/node.js:1575 +#: htdocs/luci-static/resources/view/fchomo/node.js:1584 msgid "Proxy chain" msgstr "代理鏈" -#: htdocs/luci-static/resources/view/fchomo/client.js:713 -#: htdocs/luci-static/resources/view/fchomo/client.js:1422 -#: htdocs/luci-static/resources/view/fchomo/node.js:1371 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:375 +#: htdocs/luci-static/resources/view/fchomo/client.js:723 +#: htdocs/luci-static/resources/view/fchomo/client.js:1416 +#: htdocs/luci-static/resources/view/fchomo/node.js:1388 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:373 msgid "Proxy group" msgstr "代理組" -#: htdocs/luci-static/resources/view/fchomo/client.js:1643 +#: htdocs/luci-static/resources/view/fchomo/client.js:1633 msgid "Proxy group override" msgstr "代理組覆蓋" @@ -2044,54 +2060,54 @@ msgstr "代理模式" msgid "Proxy routerself" msgstr "代理路由器自身" -#: htdocs/luci-static/resources/view/fchomo/node.js:452 +#: htdocs/luci-static/resources/view/fchomo/node.js:473 msgid "QUIC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:440 -#: htdocs/luci-static/resources/view/fchomo/server.js:431 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/server.js:417 msgid "QUIC congestion controller." msgstr "QUIC 壅塞控制器。" -#: htdocs/luci-static/resources/view/fchomo/client.js:804 -#: htdocs/luci-static/resources/view/fchomo/server.js:185 +#: htdocs/luci-static/resources/view/fchomo/client.js:814 +#: htdocs/luci-static/resources/view/fchomo/server.js:152 msgid "Quick Reload" msgstr "快速重載" -#: htdocs/luci-static/resources/view/fchomo/node.js:940 -#: htdocs/luci-static/resources/view/fchomo/server.js:895 +#: htdocs/luci-static/resources/view/fchomo/node.js:961 +#: htdocs/luci-static/resources/view/fchomo/server.js:881 msgid "REALITY" msgstr "REALITY" -#: htdocs/luci-static/resources/view/fchomo/node.js:955 +#: htdocs/luci-static/resources/view/fchomo/node.js:976 msgid "REALITY X25519MLKEM768 PQC support" msgstr "REALITY X25519MLKEM768 後量子加密支援" -#: htdocs/luci-static/resources/view/fchomo/server.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:918 msgid "REALITY certificate issued to" msgstr "REALITY 證書頒發給" -#: htdocs/luci-static/resources/view/fchomo/server.js:900 +#: htdocs/luci-static/resources/view/fchomo/server.js:886 msgid "REALITY handshake server" msgstr "REALITY 握手伺服器" -#: htdocs/luci-static/resources/view/fchomo/server.js:907 +#: htdocs/luci-static/resources/view/fchomo/server.js:893 msgid "REALITY private key" msgstr "REALITY 私鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:945 -#: htdocs/luci-static/resources/view/fchomo/server.js:922 +#: htdocs/luci-static/resources/view/fchomo/node.js:966 +#: htdocs/luci-static/resources/view/fchomo/server.js:908 msgid "REALITY public key" msgstr "REALITY 公鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:950 -#: htdocs/luci-static/resources/view/fchomo/server.js:926 +#: htdocs/luci-static/resources/view/fchomo/node.js:971 +#: htdocs/luci-static/resources/view/fchomo/server.js:912 msgid "REALITY short ID" msgstr "REALITY 標識符" -#: htdocs/luci-static/resources/view/fchomo/node.js:767 -#: htdocs/luci-static/resources/view/fchomo/server.js:602 -#: htdocs/luci-static/resources/view/fchomo/server.js:621 +#: htdocs/luci-static/resources/view/fchomo/node.js:788 +#: htdocs/luci-static/resources/view/fchomo/server.js:588 +#: htdocs/luci-static/resources/view/fchomo/server.js:607 msgid "RTT" msgstr "" @@ -2127,10 +2143,10 @@ msgstr "Redirect TCP + Tun UDP" msgid "Refresh every %s seconds." msgstr "每 %s 秒刷新。" -#: htdocs/luci-static/resources/fchomo.js:1025 -#: htdocs/luci-static/resources/view/fchomo/client.js:805 +#: htdocs/luci-static/resources/fchomo.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:815 #: htdocs/luci-static/resources/view/fchomo/global.js:193 -#: htdocs/luci-static/resources/view/fchomo/server.js:186 +#: htdocs/luci-static/resources/view/fchomo/server.js:153 msgid "Reload" msgstr "重載" @@ -2138,32 +2154,32 @@ msgstr "重載" msgid "Reload All" msgstr "重載所有" -#: htdocs/luci-static/resources/view/fchomo/node.js:1298 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:265 +#: htdocs/luci-static/resources/view/fchomo/node.js:1315 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 msgid "Remote" msgstr "遠端" -#: htdocs/luci-static/resources/view/fchomo/node.js:648 +#: htdocs/luci-static/resources/view/fchomo/node.js:669 msgid "Remote DNS resolve" msgstr "遠端 DNS 解析" -#: htdocs/luci-static/resources/fchomo.js:1184 +#: htdocs/luci-static/resources/fchomo.js:1234 msgid "Remove" msgstr "移除" -#: htdocs/luci-static/resources/fchomo.js:1189 -#: htdocs/luci-static/resources/view/fchomo/node.js:1274 -#: htdocs/luci-static/resources/view/fchomo/node.js:1276 +#: htdocs/luci-static/resources/fchomo.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1291 +#: htdocs/luci-static/resources/view/fchomo/node.js:1293 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:236 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:238 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:240 msgid "Remove idles" msgstr "移除閒置" -#: htdocs/luci-static/resources/view/fchomo/node.js:1400 +#: htdocs/luci-static/resources/view/fchomo/node.js:1417 msgid "Replace name" msgstr "名稱替換" -#: htdocs/luci-static/resources/view/fchomo/node.js:1401 +#: htdocs/luci-static/resources/view/fchomo/node.js:1418 msgid "Replace node name." msgstr "替換節點名稱" @@ -2171,13 +2187,13 @@ msgstr "替換節點名稱" msgid "Request" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1014 -#: htdocs/luci-static/resources/view/fchomo/node.js:1021 -#: htdocs/luci-static/resources/view/fchomo/server.js:974 +#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/view/fchomo/node.js:1042 +#: htdocs/luci-static/resources/view/fchomo/server.js:960 msgid "Request path" msgstr "請求路徑" -#: htdocs/luci-static/resources/view/fchomo/node.js:487 +#: htdocs/luci-static/resources/view/fchomo/node.js:508 msgid "Request timeout" msgstr "請求逾時" @@ -2190,11 +2206,11 @@ msgid "Require any" msgstr "" #: htdocs/luci-static/resources/fchomo.js:347 -#: htdocs/luci-static/resources/view/fchomo/node.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:977 msgid "Requires server support." msgstr "需要伺服器支援。" -#: htdocs/luci-static/resources/view/fchomo/node.js:637 +#: htdocs/luci-static/resources/view/fchomo/node.js:658 msgid "Reserved field bytes" msgstr "保留字段位元組" @@ -2202,16 +2218,16 @@ msgstr "保留字段位元組" msgid "Resources management" msgstr "資源管理" -#: htdocs/luci-static/resources/view/fchomo/node.js:706 +#: htdocs/luci-static/resources/view/fchomo/node.js:727 msgid "Restls script" msgstr "Restls 劇本" -#: htdocs/luci-static/resources/view/fchomo/client.js:1054 +#: htdocs/luci-static/resources/view/fchomo/client.js:1060 msgid "" "Returns hidden status in the API to hide the display of this proxy group." msgstr "在 API 傳回 hidden 狀態,以隱藏該代理組顯示。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1060 +#: htdocs/luci-static/resources/view/fchomo/client.js:1066 msgid "" "Returns the string input for icon in the API to display in this proxy group." msgstr "在 API 傳回 icon 所輸入的字串,以在該代理組顯示。" @@ -2220,17 +2236,17 @@ msgstr "在 API 傳回 icon 所輸入的字串,以在該代理組顯示。" msgid "Routing Control" msgstr "路由控制" -#: htdocs/luci-static/resources/view/fchomo/global.js:860 -#: htdocs/luci-static/resources/view/fchomo/global.js:863 +#: htdocs/luci-static/resources/view/fchomo/global.js:875 +#: htdocs/luci-static/resources/view/fchomo/global.js:878 msgid "Routing DSCP" msgstr "路由 DSCP" -#: htdocs/luci-static/resources/view/fchomo/global.js:844 +#: htdocs/luci-static/resources/view/fchomo/global.js:845 msgid "Routing GFW" msgstr "路由 GFW 流量" -#: htdocs/luci-static/resources/view/fchomo/node.js:1154 -#: htdocs/luci-static/resources/view/fchomo/node.js:1473 +#: htdocs/luci-static/resources/view/fchomo/node.js:1175 +#: htdocs/luci-static/resources/view/fchomo/node.js:1490 msgid "Routing mark" msgstr "路由標記" @@ -2246,7 +2262,7 @@ msgstr "路由模式" msgid "Routing mode of the traffic enters mihomo via firewall rules." msgstr "流量經由防火牆規則進入 mihomo 的路由模式。" -#: htdocs/luci-static/resources/view/fchomo/global.js:847 +#: htdocs/luci-static/resources/view/fchomo/global.js:862 msgid "Routing mode will be handle domain." msgstr "路由模式將處理網域。" @@ -2255,8 +2271,8 @@ msgstr "路由模式將處理網域。" msgid "Routing ports" msgstr "路由連接埠" -#: htdocs/luci-static/resources/view/fchomo/client.js:1066 -#: htdocs/luci-static/resources/view/fchomo/client.js:1075 +#: htdocs/luci-static/resources/view/fchomo/client.js:1072 +#: htdocs/luci-static/resources/view/fchomo/client.js:1081 msgid "Routing rule" msgstr "路由規則" @@ -2272,13 +2288,13 @@ msgstr "路由表 ID" msgid "Rule" msgstr "規則" -#: htdocs/luci-static/resources/view/fchomo/client.js:1607 -#: htdocs/luci-static/resources/view/fchomo/client.js:1620 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:133 +#: htdocs/luci-static/resources/view/fchomo/client.js:1597 +#: htdocs/luci-static/resources/view/fchomo/client.js:1610 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:135 msgid "Rule set" msgstr "規則集" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:357 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:355 msgid "Rule set URL" msgstr "規則集訂閱 URL" @@ -2286,11 +2302,11 @@ msgstr "規則集訂閱 URL" msgid "Ruleset" msgstr "規則集" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:185 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:183 msgid "Ruleset-URI-Scheme" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1038 +#: htdocs/luci-static/resources/fchomo.js:1088 msgid "Running" msgstr "正在運作" @@ -2314,16 +2330,16 @@ msgstr "" msgid "STUN ports" msgstr "STUN 連接埠" -#: htdocs/luci-static/resources/view/fchomo/client.js:1149 +#: htdocs/luci-static/resources/view/fchomo/client.js:1151 msgid "SUB-RULE" msgstr "SUB-RULE" -#: htdocs/luci-static/resources/view/fchomo/node.js:724 +#: htdocs/luci-static/resources/view/fchomo/node.js:745 msgid "SUoT version" msgstr "SUoT 版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:291 -#: htdocs/luci-static/resources/view/fchomo/server.js:283 +#: htdocs/luci-static/resources/view/fchomo/node.js:293 +#: htdocs/luci-static/resources/view/fchomo/server.js:250 msgid "Salamander" msgstr "Salamander" @@ -2356,19 +2372,19 @@ msgstr "以 50% 的機率發送隨機 0 到 3333 位元組的填充。" msgid "Send random ticket of 300s-600s duration for client 0-RTT reuse." msgstr "發送 300-600 秒的隨機票證,以供客戶端 0-RTT 重用。" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 -#: htdocs/luci-static/resources/view/fchomo/server.js:602 -#: htdocs/luci-static/resources/view/fchomo/server.js:793 -#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/server.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:588 +#: htdocs/luci-static/resources/view/fchomo/server.js:779 +#: htdocs/luci-static/resources/view/fchomo/server.js:794 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 msgid "Server" msgstr "服務端" -#: htdocs/luci-static/resources/view/fchomo/node.js:242 +#: htdocs/luci-static/resources/view/fchomo/node.js:244 msgid "Server address" msgstr "伺服器位址" -#: htdocs/luci-static/resources/view/fchomo/node.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:1020 msgid "Server hostname" msgstr "伺服器主機名稱" @@ -2385,22 +2401,22 @@ msgstr "服務狀態" msgid "Shadowsocks" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:506 -#: htdocs/luci-static/resources/view/fchomo/server.js:465 +#: htdocs/luci-static/resources/view/fchomo/node.js:527 +#: htdocs/luci-static/resources/view/fchomo/server.js:451 msgid "Shadowsocks chipher" msgstr "Shadowsocks 加密方法" -#: htdocs/luci-static/resources/view/fchomo/node.js:501 -#: htdocs/luci-static/resources/view/fchomo/server.js:460 +#: htdocs/luci-static/resources/view/fchomo/node.js:522 +#: htdocs/luci-static/resources/view/fchomo/server.js:446 msgid "Shadowsocks encrypt" msgstr "Shadowsocks 加密" -#: htdocs/luci-static/resources/view/fchomo/node.js:514 -#: htdocs/luci-static/resources/view/fchomo/server.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:535 +#: htdocs/luci-static/resources/view/fchomo/server.js:459 msgid "Shadowsocks password" msgstr "Shadowsocks 密碼" -#: htdocs/luci-static/resources/view/fchomo/node.js:1109 +#: htdocs/luci-static/resources/view/fchomo/node.js:1130 msgid "Show connections in the dashboard for breaking connections easier." msgstr "在面板中顯示連線以便於打斷連線。" @@ -2412,14 +2428,14 @@ msgstr "靜音" msgid "Simple round-robin all nodes" msgstr "簡單輪替所有節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:1359 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:363 +#: htdocs/luci-static/resources/view/fchomo/node.js:1376 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:361 msgid "Size limit" msgstr "大小限制" -#: htdocs/luci-static/resources/view/fchomo/client.js:1455 -#: htdocs/luci-static/resources/view/fchomo/node.js:882 -#: htdocs/luci-static/resources/view/fchomo/node.js:1451 +#: htdocs/luci-static/resources/view/fchomo/client.js:1449 +#: htdocs/luci-static/resources/view/fchomo/node.js:903 +#: htdocs/luci-static/resources/view/fchomo/node.js:1468 msgid "Skip cert verify" msgstr "跳過憑證驗證" @@ -2470,22 +2486,22 @@ msgstr "Steam 客戶端 連接埠" msgid "Steam P2P ports" msgstr "Steam P2P 連接埠" -#: htdocs/luci-static/resources/view/fchomo/client.js:1026 -#: htdocs/luci-static/resources/view/fchomo/client.js:1028 +#: htdocs/luci-static/resources/view/fchomo/client.js:1032 +#: htdocs/luci-static/resources/view/fchomo/client.js:1034 msgid "Strategy" msgstr "策略" -#: htdocs/luci-static/resources/view/fchomo/client.js:1176 -#: htdocs/luci-static/resources/view/fchomo/client.js:1185 +#: htdocs/luci-static/resources/view/fchomo/client.js:1178 +#: htdocs/luci-static/resources/view/fchomo/client.js:1187 msgid "Sub rule" msgstr "子規則" -#: htdocs/luci-static/resources/view/fchomo/client.js:1235 +#: htdocs/luci-static/resources/view/fchomo/client.js:1233 msgid "Sub rule group" msgstr "子規則組" -#: htdocs/luci-static/resources/fchomo.js:573 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:209 +#: htdocs/luci-static/resources/fchomo.js:643 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:207 msgid "Successfully imported %s %s of total %s." msgstr "已成功匯入 %s 個%s (共 %s 個)。" @@ -2493,7 +2509,7 @@ msgstr "已成功匯入 %s 個%s (共 %s 個)。" msgid "Successfully updated." msgstr "更新成功。" -#: htdocs/luci-static/resources/fchomo.js:1518 +#: htdocs/luci-static/resources/fchomo.js:1568 msgid "Successfully uploaded." msgstr "已成功上傳。" @@ -2502,7 +2518,7 @@ msgstr "已成功上傳。" msgid "Sudoku" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:182 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:180 msgid "" "Supports rule-set links of type: %s and format: %s." "
" @@ -2514,7 +2530,7 @@ msgstr "" msgid "System" msgstr "系統" -#: htdocs/luci-static/resources/view/fchomo/client.js:54 +#: htdocs/luci-static/resources/view/fchomo/client.js:56 msgid "System DNS" msgstr "系統 DNS" @@ -2532,8 +2548,8 @@ msgstr "系統 DNS" #: htdocs/luci-static/resources/fchomo.js:167 #: htdocs/luci-static/resources/fchomo.js:168 #: htdocs/luci-static/resources/fchomo.js:173 -#: htdocs/luci-static/resources/view/fchomo/client.js:517 -#: htdocs/luci-static/resources/view/fchomo/client.js:607 +#: htdocs/luci-static/resources/view/fchomo/client.js:527 +#: htdocs/luci-static/resources/view/fchomo/client.js:617 msgid "TCP" msgstr "TCP" @@ -2541,7 +2557,7 @@ msgstr "TCP" msgid "TCP concurrency" msgstr "TCP 併發" -#: htdocs/luci-static/resources/view/fchomo/node.js:1102 +#: htdocs/luci-static/resources/view/fchomo/node.js:1123 msgid "TCP only" msgstr "僅 TCP" @@ -2564,29 +2580,29 @@ msgstr "TCP-Keep-Alive 間隔" msgid "TCP/UDP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1133 -#: htdocs/luci-static/resources/view/fchomo/node.js:1418 +#: htdocs/luci-static/resources/view/fchomo/node.js:1154 +#: htdocs/luci-static/resources/view/fchomo/node.js:1435 msgid "TFO" msgstr "TCP 快速開啟 (TFO)" #: htdocs/luci-static/resources/view/fchomo/global.js:529 -#: htdocs/luci-static/resources/view/fchomo/node.js:673 -#: htdocs/luci-static/resources/view/fchomo/node.js:799 -#: htdocs/luci-static/resources/view/fchomo/server.js:756 +#: htdocs/luci-static/resources/view/fchomo/node.js:694 +#: htdocs/luci-static/resources/view/fchomo/node.js:820 +#: htdocs/luci-static/resources/view/fchomo/server.js:742 msgid "TLS" msgstr "TLS" -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:851 +#: htdocs/luci-static/resources/view/fchomo/server.js:773 msgid "TLS ALPN" msgstr "TLS ALPN" -#: htdocs/luci-static/resources/view/fchomo/node.js:824 +#: htdocs/luci-static/resources/view/fchomo/node.js:845 msgid "TLS SNI" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:222 -#: htdocs/luci-static/resources/view/fchomo/server.js:206 +#: htdocs/luci-static/resources/view/fchomo/node.js:224 +#: htdocs/luci-static/resources/view/fchomo/server.js:173 msgid "TLS fields" msgstr "TLS欄位" @@ -2599,39 +2615,39 @@ msgstr "" msgid "TURN ports" msgstr "TURN 連接埠" -#: htdocs/luci-static/resources/view/fchomo/server.js:276 +#: htdocs/luci-static/resources/view/fchomo/server.js:243 msgid "" "Tell the client to use the BBR flow control algorithm instead of Hysteria CC." msgstr "讓客戶端使用 BBR 流控演算法。" -#: htdocs/luci-static/resources/view/fchomo/node.js:596 -#: htdocs/luci-static/resources/view/fchomo/node.js:603 +#: htdocs/luci-static/resources/view/fchomo/node.js:617 +#: htdocs/luci-static/resources/view/fchomo/node.js:624 msgid "The %s address used by local machine in the Wireguard network." msgstr "WireGuard 網路中使用的本機 %s 位址。" -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:808 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 +#: htdocs/luci-static/resources/view/fchomo/server.js:794 msgid "The %s private key, in PEM format." msgstr "%s私鑰,需要 PEM 格式。" #: htdocs/luci-static/resources/view/fchomo/global.js:556 -#: htdocs/luci-static/resources/view/fchomo/node.js:891 -#: htdocs/luci-static/resources/view/fchomo/server.js:793 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/view/fchomo/node.js:912 +#: htdocs/luci-static/resources/view/fchomo/server.js:779 +#: htdocs/luci-static/resources/view/fchomo/server.js:817 msgid "The %s public key, in PEM format." msgstr "%s公鑰,需要 PEM 格式。" -#: htdocs/luci-static/resources/view/fchomo/node.js:925 +#: htdocs/luci-static/resources/view/fchomo/node.js:946 msgid "" "The ECH parameter of the HTTPS record for the domain. Leave empty to resolve " "via DNS." msgstr "網域的 HTTPS 記錄的 ECH 參數。留空則透過 DNS 解析。" -#: htdocs/luci-static/resources/view/fchomo/node.js:372 +#: htdocs/luci-static/resources/view/fchomo/node.js:374 msgid "The ED25519 available private key or UUID provided by Sudoku server." msgstr "Sudoku 伺服器提供的 ED25519 可用私鑰 或 UUID。" -#: htdocs/luci-static/resources/view/fchomo/server.js:333 +#: htdocs/luci-static/resources/view/fchomo/server.js:300 msgid "The ED25519 master public key or UUID generated by Sudoku." msgstr "Sudoku 產生的 ED25519 主公鑰 或 UUID。" @@ -2639,42 +2655,42 @@ msgstr "Sudoku 產生的 ED25519 主公鑰 或 UUID。" msgid "The default value is 2:00 every day." msgstr "預設值為每天 2:00。" -#: htdocs/luci-static/resources/view/fchomo/node.js:780 -#: htdocs/luci-static/resources/view/fchomo/server.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:801 +#: htdocs/luci-static/resources/view/fchomo/server.js:620 msgid "" "The first padding must have a probability of 100% and at least 35 bytes." msgstr "首個填充必須為 100% 的機率並且至少 35 位元組。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1666 +#: htdocs/luci-static/resources/view/fchomo/client.js:1656 msgid "The matching %s will be deemed as not-poisoned." msgstr "匹配 %s 的將被視為未被投毒汙染。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1675 -#: htdocs/luci-static/resources/view/fchomo/client.js:1679 -#: htdocs/luci-static/resources/view/fchomo/client.js:1684 +#: htdocs/luci-static/resources/view/fchomo/client.js:1665 +#: htdocs/luci-static/resources/view/fchomo/client.js:1669 +#: htdocs/luci-static/resources/view/fchomo/client.js:1674 msgid "The matching %s will be deemed as poisoned." msgstr "匹配 %s 的將被視為已被投毒汙染。" -#: htdocs/luci-static/resources/view/fchomo/node.js:778 -#: htdocs/luci-static/resources/view/fchomo/server.js:632 +#: htdocs/luci-static/resources/view/fchomo/node.js:799 +#: htdocs/luci-static/resources/view/fchomo/server.js:618 msgid "The server and client can set different padding parameters." msgstr "伺服器和客戶端可以設定不同的填充參數。" #: htdocs/luci-static/resources/view/fchomo/global.js:600 -#: htdocs/luci-static/resources/view/fchomo/server.js:889 +#: htdocs/luci-static/resources/view/fchomo/server.js:875 msgid "This ECH parameter needs to be added to the HTTPS record of the domain." msgstr "此 ECH 參數需要加入到網域的 HTTPS 記錄中。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1458 -#: htdocs/luci-static/resources/view/fchomo/node.js:885 -#: htdocs/luci-static/resources/view/fchomo/node.js:1454 +#: htdocs/luci-static/resources/view/fchomo/client.js:1452 +#: htdocs/luci-static/resources/view/fchomo/node.js:906 +#: htdocs/luci-static/resources/view/fchomo/node.js:1471 msgid "" "This is DANGEROUS, your traffic is almost like " "PLAIN TEXT! Use at your own risk!" msgstr "" "這是危險行為,您的流量將幾乎等同於明文!使用風險自負!" -#: htdocs/luci-static/resources/view/fchomo/node.js:457 +#: htdocs/luci-static/resources/view/fchomo/node.js:478 msgid "" "This is the TUIC port of the SUoT protocol, designed to provide a QUIC " "stream based UDP relay mode that TUIC does not provide." @@ -2694,7 +2710,7 @@ msgid "" msgstr "" "要啟用 Tun 支持,您需要安裝 ip-fullkmod-tun。" -#: htdocs/luci-static/resources/view/fchomo/global.js:852 +#: htdocs/luci-static/resources/view/fchomo/global.js:867 msgid "To enable, you need to install dnsmasq-full." msgstr "要啟用,您需要安裝 dnsmasq-full。" @@ -2706,32 +2722,32 @@ msgstr "Tproxy Fwmark/fwmask" msgid "Tproxy port" msgstr "Tproxy 連接埠" -#: htdocs/luci-static/resources/view/fchomo/node.js:1610 +#: htdocs/luci-static/resources/view/fchomo/node.js:1627 msgid "Transit proxy group" msgstr "中轉代理組" -#: htdocs/luci-static/resources/view/fchomo/node.js:1616 +#: htdocs/luci-static/resources/view/fchomo/node.js:1633 msgid "Transit proxy node" msgstr "中轉代理節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:347 -#: htdocs/luci-static/resources/view/fchomo/node.js:962 -#: htdocs/luci-static/resources/view/fchomo/server.js:320 -#: htdocs/luci-static/resources/view/fchomo/server.js:940 +#: htdocs/luci-static/resources/view/fchomo/node.js:349 +#: htdocs/luci-static/resources/view/fchomo/node.js:983 +#: htdocs/luci-static/resources/view/fchomo/server.js:287 +#: htdocs/luci-static/resources/view/fchomo/server.js:926 msgid "Transport" msgstr "傳輸層" -#: htdocs/luci-static/resources/view/fchomo/node.js:223 -#: htdocs/luci-static/resources/view/fchomo/server.js:207 +#: htdocs/luci-static/resources/view/fchomo/node.js:225 +#: htdocs/luci-static/resources/view/fchomo/server.js:174 msgid "Transport fields" msgstr "傳輸層欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:967 -#: htdocs/luci-static/resources/view/fchomo/server.js:945 +#: htdocs/luci-static/resources/view/fchomo/node.js:988 +#: htdocs/luci-static/resources/view/fchomo/server.js:931 msgid "Transport type" msgstr "傳輸層類型" -#: htdocs/luci-static/resources/view/fchomo/client.js:732 +#: htdocs/luci-static/resources/view/fchomo/client.js:742 msgid "Treat the destination IP as the source IP." msgstr "將 目標 IP 視為 來源 IP。" @@ -2756,16 +2772,16 @@ msgstr "Tun 設定" msgid "Tun stack." msgstr "Tun 堆栈" -#: htdocs/luci-static/resources/view/fchomo/client.js:458 -#: htdocs/luci-static/resources/view/fchomo/client.js:571 -#: htdocs/luci-static/resources/view/fchomo/client.js:665 -#: htdocs/luci-static/resources/view/fchomo/client.js:910 -#: htdocs/luci-static/resources/view/fchomo/client.js:1604 -#: htdocs/luci-static/resources/view/fchomo/node.js:236 -#: htdocs/luci-static/resources/view/fchomo/node.js:1296 -#: htdocs/luci-static/resources/view/fchomo/node.js:1581 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 -#: htdocs/luci-static/resources/view/fchomo/server.js:226 +#: htdocs/luci-static/resources/view/fchomo/client.js:468 +#: htdocs/luci-static/resources/view/fchomo/client.js:581 +#: htdocs/luci-static/resources/view/fchomo/client.js:675 +#: htdocs/luci-static/resources/view/fchomo/client.js:916 +#: htdocs/luci-static/resources/view/fchomo/client.js:1594 +#: htdocs/luci-static/resources/view/fchomo/node.js:238 +#: htdocs/luci-static/resources/view/fchomo/node.js:1313 +#: htdocs/luci-static/resources/view/fchomo/node.js:1598 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:261 +#: htdocs/luci-static/resources/view/fchomo/server.js:193 msgid "Type" msgstr "類型" @@ -2774,11 +2790,11 @@ msgstr "類型" #: htdocs/luci-static/resources/fchomo.js:170 #: htdocs/luci-static/resources/fchomo.js:171 #: htdocs/luci-static/resources/fchomo.js:172 -#: htdocs/luci-static/resources/view/fchomo/client.js:516 -#: htdocs/luci-static/resources/view/fchomo/client.js:606 -#: htdocs/luci-static/resources/view/fchomo/node.js:713 -#: htdocs/luci-static/resources/view/fchomo/node.js:1428 -#: htdocs/luci-static/resources/view/fchomo/server.js:539 +#: htdocs/luci-static/resources/view/fchomo/client.js:526 +#: htdocs/luci-static/resources/view/fchomo/client.js:616 +#: htdocs/luci-static/resources/view/fchomo/node.js:734 +#: htdocs/luci-static/resources/view/fchomo/node.js:1445 +#: htdocs/luci-static/resources/view/fchomo/server.js:525 msgid "UDP" msgstr "UDP" @@ -2786,19 +2802,19 @@ msgstr "UDP" msgid "UDP NAT expiration time" msgstr "UDP NAT 過期時間" -#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 msgid "UDP over stream" msgstr "UDP over stream" -#: htdocs/luci-static/resources/view/fchomo/node.js:462 +#: htdocs/luci-static/resources/view/fchomo/node.js:483 msgid "UDP over stream version" msgstr "UDP over stream 版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:449 +#: htdocs/luci-static/resources/view/fchomo/node.js:470 msgid "UDP packet relay mode." msgstr "UDP 包中繼模式。" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 +#: htdocs/luci-static/resources/view/fchomo/node.js:469 msgid "UDP relay mode" msgstr "UDP 中繼模式" @@ -2806,15 +2822,15 @@ msgstr "UDP 中繼模式" msgid "URL test" msgstr "自動選擇" -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/node.js:545 -#: htdocs/luci-static/resources/view/fchomo/server.js:330 -#: htdocs/luci-static/resources/view/fchomo/server.js:424 -#: htdocs/luci-static/resources/view/fchomo/server.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:448 +#: htdocs/luci-static/resources/view/fchomo/node.js:566 +#: htdocs/luci-static/resources/view/fchomo/server.js:297 +#: htdocs/luci-static/resources/view/fchomo/server.js:410 +#: htdocs/luci-static/resources/view/fchomo/server.js:474 msgid "UUID" msgstr "UUID" -#: htdocs/luci-static/resources/fchomo.js:1083 +#: htdocs/luci-static/resources/fchomo.js:1133 msgid "Unable to download unsupported type: %s" msgstr "無法下載不支援的類型: %s" @@ -2839,8 +2855,8 @@ msgstr "未知錯誤。" msgid "Unknown error: %s" msgstr "未知錯誤:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:718 -#: htdocs/luci-static/resources/view/fchomo/node.js:1433 +#: htdocs/luci-static/resources/view/fchomo/node.js:739 +#: htdocs/luci-static/resources/view/fchomo/node.js:1450 msgid "UoT" msgstr "UDP over TCP (UoT)" @@ -2848,22 +2864,29 @@ msgstr "UDP over TCP (UoT)" msgid "Update failed." msgstr "更新失敗。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1365 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:369 +#: htdocs/luci-static/resources/view/fchomo/node.js:1382 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:367 msgid "Update interval" msgstr "更新間隔" -#: htdocs/luci-static/resources/view/fchomo/node.js:1120 +#: htdocs/luci-static/resources/view/fchomo/node.js:426 +#: htdocs/luci-static/resources/view/fchomo/server.js:404 +msgid "" +"Uplink keeps the Sudoku protocol, and downlink characteristics are " +"consistent with uplink characteristics." +msgstr "上行鏈路保持數獨協議,下行鏈路特性與上行鏈路特性一致。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1141 msgid "Upload bandwidth" msgstr "上傳頻寬" -#: htdocs/luci-static/resources/view/fchomo/node.js:1121 +#: htdocs/luci-static/resources/view/fchomo/node.js:1142 msgid "Upload bandwidth in Mbps." msgstr "上傳頻寬(單位:Mbps)。" -#: htdocs/luci-static/resources/view/fchomo/node.js:896 -#: htdocs/luci-static/resources/view/fchomo/server.js:799 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 +#: htdocs/luci-static/resources/view/fchomo/node.js:917 +#: htdocs/luci-static/resources/view/fchomo/server.js:785 +#: htdocs/luci-static/resources/view/fchomo/server.js:825 msgid "Upload certificate" msgstr "上傳憑證" @@ -2871,41 +2894,41 @@ msgstr "上傳憑證" msgid "Upload initial package" msgstr "上傳初始資源包" -#: htdocs/luci-static/resources/view/fchomo/node.js:910 -#: htdocs/luci-static/resources/view/fchomo/server.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +#: htdocs/luci-static/resources/view/fchomo/server.js:800 msgid "Upload key" msgstr "上傳金鑰" #: htdocs/luci-static/resources/view/fchomo/global.js:306 -#: htdocs/luci-static/resources/view/fchomo/node.js:899 -#: htdocs/luci-static/resources/view/fchomo/node.js:913 -#: htdocs/luci-static/resources/view/fchomo/server.js:802 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:842 +#: htdocs/luci-static/resources/view/fchomo/node.js:920 +#: htdocs/luci-static/resources/view/fchomo/node.js:934 +#: htdocs/luci-static/resources/view/fchomo/server.js:788 +#: htdocs/luci-static/resources/view/fchomo/server.js:803 +#: htdocs/luci-static/resources/view/fchomo/server.js:828 msgid "Upload..." msgstr "上傳..." -#: htdocs/luci-static/resources/view/fchomo/client.js:1276 +#: htdocs/luci-static/resources/view/fchomo/client.js:1274 msgid "" "Used to resolve domains that can be directly connected. Can use domestic DNS " "servers or ECS." msgstr "用於解析可直連的網域。可使用國內 DNS 伺服器或 ECS。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1278 +#: htdocs/luci-static/resources/view/fchomo/client.js:1276 msgid "" "Used to resolve domains you want to proxy. Recommended to configure %s for " "DNS servers." msgstr "用於解析想要代理的網域。建議為 DNS 伺服器配置%s。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1260 +#: htdocs/luci-static/resources/view/fchomo/client.js:1258 msgid "Used to resolve the domain of the DNS server. Must be IP." msgstr "用於解析 DNS 伺服器的網域。必須是 IP。" -#: htdocs/luci-static/resources/view/fchomo/client.js:1267 +#: htdocs/luci-static/resources/view/fchomo/client.js:1265 msgid "Used to resolve the domain of the Proxy node." msgstr "用於解析代理節點的網域。" -#: htdocs/luci-static/resources/view/fchomo/node.js:825 +#: htdocs/luci-static/resources/view/fchomo/node.js:846 msgid "Used to verify the hostname on the returned certificates." msgstr "用於驗證傳回的憑證上的主機名稱。" @@ -2913,8 +2936,8 @@ msgstr "用於驗證傳回的憑證上的主機名稱。" msgid "User Authentication" msgstr "使用者認證" -#: htdocs/luci-static/resources/view/fchomo/node.js:254 -#: htdocs/luci-static/resources/view/fchomo/server.js:249 +#: htdocs/luci-static/resources/view/fchomo/node.js:256 +#: htdocs/luci-static/resources/view/fchomo/server.js:216 msgid "Username" msgstr "使用者名稱" @@ -2922,11 +2945,11 @@ msgstr "使用者名稱" msgid "Users filter mode" msgstr "使用者過濾模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:1050 +#: htdocs/luci-static/resources/view/fchomo/node.js:1071 msgid "V2ray HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +#: htdocs/luci-static/resources/view/fchomo/node.js:1076 msgid "V2ray HTTPUpgrade fast open" msgstr "" @@ -2940,9 +2963,9 @@ msgstr "" msgid "VMess" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1302 -#: htdocs/luci-static/resources/view/fchomo/node.js:1587 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:306 +#: htdocs/luci-static/resources/view/fchomo/node.js:1319 +#: htdocs/luci-static/resources/view/fchomo/node.js:1604 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:304 msgid "Value" msgstr "可視化值" @@ -2950,18 +2973,18 @@ msgstr "可視化值" msgid "Verify if given" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:418 -#: htdocs/luci-static/resources/view/fchomo/node.js:692 -#: htdocs/luci-static/resources/view/fchomo/server.js:530 +#: htdocs/luci-static/resources/view/fchomo/node.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:713 +#: htdocs/luci-static/resources/view/fchomo/server.js:516 msgid "Version" msgstr "版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:700 +#: htdocs/luci-static/resources/view/fchomo/node.js:721 msgid "Version hint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:221 -#: htdocs/luci-static/resources/view/fchomo/server.js:205 +#: htdocs/luci-static/resources/view/fchomo/node.js:223 +#: htdocs/luci-static/resources/view/fchomo/server.js:172 msgid "Vless Encryption fields" msgstr "Vless Encryption 欄位" @@ -2973,16 +2996,25 @@ msgstr "以 75% 的機率等待隨機 0-111 毫秒。" msgid "Warning" msgstr "警告" -#: htdocs/luci-static/resources/view/fchomo/node.js:972 -#: htdocs/luci-static/resources/view/fchomo/node.js:983 -#: htdocs/luci-static/resources/view/fchomo/node.js:988 -#: htdocs/luci-static/resources/view/fchomo/server.js:947 -#: htdocs/luci-static/resources/view/fchomo/server.js:958 -#: htdocs/luci-static/resources/view/fchomo/server.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:993 +#: htdocs/luci-static/resources/view/fchomo/node.js:1004 +#: htdocs/luci-static/resources/view/fchomo/node.js:1009 +#: htdocs/luci-static/resources/view/fchomo/server.js:933 +#: htdocs/luci-static/resources/view/fchomo/server.js:944 +#: htdocs/luci-static/resources/view/fchomo/server.js:949 msgid "WebSocket" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:165 +#: htdocs/luci-static/resources/view/fchomo/node.js:425 +#: htdocs/luci-static/resources/view/fchomo/server.js:403 +msgid "" +"When disabled, downlink ciphertext is split into 6-bit segments, reusing the " +"original padding pool and obfuscate type to reduce downlink overhead." +msgstr "" +"停用後,下行密文將分成 6 位元片段,重複使用原始填充池和混淆類型,以減少下行開" +"銷。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:132 msgid "When used as a server, HomeProxy is a better choice." msgstr "用作服務端時,HomeProxy 是更好的選擇。" @@ -2994,27 +3026,27 @@ msgstr "白名單" msgid "WireGuard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:617 +#: htdocs/luci-static/resources/view/fchomo/node.js:638 msgid "WireGuard peer public key." msgstr "WireGuard 對端公鑰。" -#: htdocs/luci-static/resources/view/fchomo/node.js:624 +#: htdocs/luci-static/resources/view/fchomo/node.js:645 msgid "WireGuard pre-shared key." msgstr "WireGuard 預先共用金鑰。" -#: htdocs/luci-static/resources/view/fchomo/node.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:630 msgid "WireGuard requires base64-encoded private keys." msgstr "WireGuard 要求 base64 編碼的私鑰。" -#: htdocs/luci-static/resources/view/fchomo/server.js:593 +#: htdocs/luci-static/resources/view/fchomo/server.js:579 msgid "XOR mode" msgstr "XOR 模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:590 +#: htdocs/luci-static/resources/view/fchomo/node.js:611 msgid "Xudp (Xray-core)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:285 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:283 msgid "Yaml text" msgstr "Yaml 格式文本" @@ -3022,14 +3054,14 @@ msgstr "Yaml 格式文本" msgid "YouTube" msgstr "YouTube" -#: htdocs/luci-static/resources/fchomo.js:1500 +#: htdocs/luci-static/resources/fchomo.js:1550 msgid "Your %s was successfully uploaded. Size: %sB." msgstr "您的 %s 已成功上傳。大小:%sB。" #: htdocs/luci-static/resources/fchomo.js:289 #: htdocs/luci-static/resources/fchomo.js:302 #: htdocs/luci-static/resources/fchomo.js:307 -#: htdocs/luci-static/resources/view/fchomo/node.js:570 +#: htdocs/luci-static/resources/view/fchomo/node.js:591 msgid "aes-128-gcm" msgstr "" @@ -3042,18 +3074,18 @@ msgstr "" msgid "aes-256-gcm" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:567 +#: htdocs/luci-static/resources/view/fchomo/node.js:588 msgid "auto" msgstr "自動" -#: htdocs/luci-static/resources/view/fchomo/node.js:444 -#: htdocs/luci-static/resources/view/fchomo/server.js:435 +#: htdocs/luci-static/resources/view/fchomo/node.js:465 +#: htdocs/luci-static/resources/view/fchomo/server.js:421 msgid "bbr" msgstr "bbr" -#: htdocs/luci-static/resources/view/fchomo/node.js:901 -#: htdocs/luci-static/resources/view/fchomo/server.js:804 -#: htdocs/luci-static/resources/view/fchomo/server.js:844 +#: htdocs/luci-static/resources/view/fchomo/node.js:922 +#: htdocs/luci-static/resources/view/fchomo/server.js:790 +#: htdocs/luci-static/resources/view/fchomo/server.js:830 msgid "certificate" msgstr "憑證" @@ -3063,17 +3095,17 @@ msgstr "憑證" msgid "chacha20-ietf-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:571 +#: htdocs/luci-static/resources/view/fchomo/node.js:592 msgid "chacha20-poly1305" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:442 -#: htdocs/luci-static/resources/view/fchomo/server.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:463 +#: htdocs/luci-static/resources/view/fchomo/server.js:419 msgid "cubic" msgstr "cubic" -#: htdocs/luci-static/resources/view/fchomo/server.js:545 -#: htdocs/luci-static/resources/view/fchomo/server.js:576 +#: htdocs/luci-static/resources/view/fchomo/server.js:531 +#: htdocs/luci-static/resources/view/fchomo/server.js:562 msgid "decryption" msgstr "decryption" @@ -3081,27 +3113,27 @@ msgstr "decryption" msgid "dnsmasq selects upstream on its own. (may affect CDN accuracy)" msgstr "dnsmasq 自行選擇上游服務器。 (可能影響 CDN 準確性)" -#: htdocs/luci-static/resources/view/fchomo/node.js:1445 +#: htdocs/luci-static/resources/view/fchomo/node.js:1462 msgid "down" msgstr "Hysteria 下載速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:732 -#: htdocs/luci-static/resources/view/fchomo/node.js:755 -#: htdocs/luci-static/resources/view/fchomo/server.js:580 +#: htdocs/luci-static/resources/view/fchomo/node.js:753 +#: htdocs/luci-static/resources/view/fchomo/node.js:776 +#: htdocs/luci-static/resources/view/fchomo/server.js:566 msgid "encryption" msgstr "encryption" -#: htdocs/luci-static/resources/view/fchomo/node.js:971 -#: htdocs/luci-static/resources/view/fchomo/node.js:982 -#: htdocs/luci-static/resources/view/fchomo/node.js:987 -#: htdocs/luci-static/resources/view/fchomo/server.js:946 -#: htdocs/luci-static/resources/view/fchomo/server.js:957 -#: htdocs/luci-static/resources/view/fchomo/server.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:992 +#: htdocs/luci-static/resources/view/fchomo/node.js:1003 +#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/server.js:932 +#: htdocs/luci-static/resources/view/fchomo/server.js:943 +#: htdocs/luci-static/resources/view/fchomo/server.js:948 msgid "gRPC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 -#: htdocs/luci-static/resources/view/fchomo/server.js:981 +#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +#: htdocs/luci-static/resources/view/fchomo/server.js:967 msgid "gRPC service name" msgstr "gRPC 服務名稱" @@ -3109,11 +3141,11 @@ msgstr "gRPC 服務名稱" msgid "gVisor" msgstr "gVisor" -#: htdocs/luci-static/resources/view/fchomo/node.js:1071 +#: htdocs/luci-static/resources/view/fchomo/node.js:1092 msgid "h2mux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:745 +#: htdocs/luci-static/resources/view/fchomo/server.js:731 msgid "least one keypair required" msgstr "至少需要一對密鑰" @@ -3121,13 +3153,13 @@ msgstr "至少需要一對密鑰" msgid "metacubexd" msgstr "metacubexd" -#: htdocs/luci-static/resources/view/fchomo/client.js:883 -#: htdocs/luci-static/resources/view/fchomo/client.js:1122 -#: htdocs/luci-static/resources/view/fchomo/client.js:1218 -#: htdocs/luci-static/resources/view/fchomo/client.js:1359 -#: htdocs/luci-static/resources/view/fchomo/client.js:1581 -#: htdocs/luci-static/resources/view/fchomo/node.js:1268 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:226 +#: htdocs/luci-static/resources/view/fchomo/client.js:889 +#: htdocs/luci-static/resources/view/fchomo/client.js:1124 +#: htdocs/luci-static/resources/view/fchomo/client.js:1216 +#: htdocs/luci-static/resources/view/fchomo/client.js:1353 +#: htdocs/luci-static/resources/view/fchomo/client.js:1571 +#: htdocs/luci-static/resources/view/fchomo/node.js:1285 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:224 msgid "mihomo config" msgstr "mihomo 配置" @@ -3135,35 +3167,35 @@ msgstr "mihomo 配置" msgid "mlkem768x25519plus" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1137 -#: htdocs/luci-static/resources/view/fchomo/node.js:1423 +#: htdocs/luci-static/resources/view/fchomo/node.js:1158 +#: htdocs/luci-static/resources/view/fchomo/node.js:1440 msgid "mpTCP" msgstr "多路徑 TCP (mpTCP)" -#: htdocs/luci-static/resources/view/fchomo/node.js:443 -#: htdocs/luci-static/resources/view/fchomo/server.js:434 +#: htdocs/luci-static/resources/view/fchomo/node.js:464 +#: htdocs/luci-static/resources/view/fchomo/server.js:420 msgid "new_reno" msgstr "new_reno" -#: htdocs/luci-static/resources/view/fchomo/client.js:749 +#: htdocs/luci-static/resources/view/fchomo/client.js:759 msgid "no-resolve" msgstr "no-resolve" -#: htdocs/luci-static/resources/fchomo.js:1251 -#: htdocs/luci-static/resources/fchomo.js:1346 -#: htdocs/luci-static/resources/fchomo.js:1381 -#: htdocs/luci-static/resources/fchomo.js:1393 +#: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1396 +#: htdocs/luci-static/resources/fchomo.js:1431 +#: htdocs/luci-static/resources/fchomo.js:1443 msgid "non-empty value" msgstr "非空值" #: htdocs/luci-static/resources/fchomo.js:287 #: htdocs/luci-static/resources/fchomo.js:301 #: htdocs/luci-static/resources/fchomo.js:313 -#: htdocs/luci-static/resources/view/fchomo/node.js:568 -#: htdocs/luci-static/resources/view/fchomo/node.js:588 -#: htdocs/luci-static/resources/view/fchomo/node.js:661 -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:302 -#: htdocs/luci-static/resources/view/fchomo/server.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:589 +#: htdocs/luci-static/resources/view/fchomo/node.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:682 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:300 +#: htdocs/luci-static/resources/view/fchomo/server.js:497 msgid "none" msgstr "無" @@ -3171,7 +3203,7 @@ msgstr "無" msgid "not found" msgstr "未找到" -#: htdocs/luci-static/resources/view/fchomo/client.js:900 +#: htdocs/luci-static/resources/view/fchomo/client.js:906 msgid "not included \",\"" msgstr "不包含 \",\"" @@ -3179,20 +3211,20 @@ msgstr "不包含 \",\"" msgid "null" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:662 +#: htdocs/luci-static/resources/view/fchomo/node.js:683 msgid "obfs-simple" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1403 +#: htdocs/luci-static/resources/view/fchomo/node.js:1420 msgid "override.proxy-name" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:589 +#: htdocs/luci-static/resources/view/fchomo/node.js:610 msgid "packet addr (v2ray-core v5+)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:915 -#: htdocs/luci-static/resources/view/fchomo/server.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:936 +#: htdocs/luci-static/resources/view/fchomo/server.js:805 msgid "private key" msgstr "私鑰" @@ -3200,33 +3232,33 @@ msgstr "私鑰" msgid "razord-meta" msgstr "razord-meta" -#: htdocs/luci-static/resources/view/fchomo/client.js:1055 #: htdocs/luci-static/resources/view/fchomo/client.js:1061 +#: htdocs/luci-static/resources/view/fchomo/client.js:1067 msgid "requires front-end adaptation using the API." msgstr "需要使用 API 的前端適配。" -#: htdocs/luci-static/resources/view/fchomo/node.js:666 +#: htdocs/luci-static/resources/view/fchomo/node.js:687 msgid "restls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/ruleset.js:210 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:208 msgid "rule-set" msgstr "規則集" -#: htdocs/luci-static/resources/view/fchomo/node.js:665 -#: htdocs/luci-static/resources/view/fchomo/server.js:512 +#: htdocs/luci-static/resources/view/fchomo/node.js:686 +#: htdocs/luci-static/resources/view/fchomo/server.js:498 msgid "shadow-tls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1069 +#: htdocs/luci-static/resources/view/fchomo/node.js:1090 msgid "smux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/client.js:731 +#: htdocs/luci-static/resources/view/fchomo/client.js:741 msgid "src" msgstr "src" -#: htdocs/luci-static/resources/view/fchomo/server.js:329 +#: htdocs/luci-static/resources/view/fchomo/server.js:296 msgid "sudoku-keypair" msgstr "" @@ -3242,62 +3274,62 @@ msgstr "獨立 UCI 識別" msgid "unique identifier" msgstr "獨立標識" -#: htdocs/luci-static/resources/fchomo.js:1402 +#: htdocs/luci-static/resources/fchomo.js:1452 msgid "unique value" msgstr "獨立值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1439 +#: htdocs/luci-static/resources/view/fchomo/node.js:1456 msgid "up" msgstr "Hysteria 上傳速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:419 -#: htdocs/luci-static/resources/view/fchomo/node.js:463 -#: htdocs/luci-static/resources/view/fchomo/node.js:693 -#: htdocs/luci-static/resources/view/fchomo/node.js:725 -#: htdocs/luci-static/resources/view/fchomo/server.js:531 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 +#: htdocs/luci-static/resources/view/fchomo/node.js:484 +#: htdocs/luci-static/resources/view/fchomo/node.js:714 +#: htdocs/luci-static/resources/view/fchomo/node.js:746 +#: htdocs/luci-static/resources/view/fchomo/server.js:517 msgid "v1" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:420 -#: htdocs/luci-static/resources/view/fchomo/node.js:694 -#: htdocs/luci-static/resources/view/fchomo/node.js:726 -#: htdocs/luci-static/resources/view/fchomo/server.js:532 +#: htdocs/luci-static/resources/view/fchomo/node.js:441 +#: htdocs/luci-static/resources/view/fchomo/node.js:715 +#: htdocs/luci-static/resources/view/fchomo/node.js:747 +#: htdocs/luci-static/resources/view/fchomo/server.js:518 msgid "v2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:421 -#: htdocs/luci-static/resources/view/fchomo/node.js:695 -#: htdocs/luci-static/resources/view/fchomo/server.js:533 +#: htdocs/luci-static/resources/view/fchomo/node.js:442 +#: htdocs/luci-static/resources/view/fchomo/node.js:716 +#: htdocs/luci-static/resources/view/fchomo/server.js:519 msgid "v3" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1298 -#: htdocs/luci-static/resources/fchomo.js:1301 +#: htdocs/luci-static/resources/fchomo.js:1348 +#: htdocs/luci-static/resources/fchomo.js:1351 msgid "valid JSON format" msgstr "有效的 JSON 格式" -#: htdocs/luci-static/resources/view/fchomo/node.js:875 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 msgid "valid SHA256 string with %d characters" msgstr "包含 %d 個字元的有效 SHA256 字串" -#: htdocs/luci-static/resources/fchomo.js:1323 -#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1373 +#: htdocs/luci-static/resources/fchomo.js:1376 msgid "valid URL" msgstr "有效網址" -#: htdocs/luci-static/resources/fchomo.js:1336 +#: htdocs/luci-static/resources/fchomo.js:1386 msgid "valid base64 key with %d characters" msgstr "包含 %d 個字元的有效 base64 金鑰" -#: htdocs/luci-static/resources/fchomo.js:1383 +#: htdocs/luci-static/resources/fchomo.js:1433 msgid "valid key length with %d characters" msgstr "包含 %d 個字元的有效金鑰" -#: htdocs/luci-static/resources/fchomo.js:1261 +#: htdocs/luci-static/resources/fchomo.js:1311 msgid "valid port value" msgstr "有效連接埠值" -#: htdocs/luci-static/resources/fchomo.js:1311 +#: htdocs/luci-static/resources/fchomo.js:1361 msgid "valid uuid" msgstr "有效 uuid" @@ -3317,7 +3349,7 @@ msgstr "" msgid "yacd-meta" msgstr "yacd-meta" -#: htdocs/luci-static/resources/view/fchomo/node.js:1070 +#: htdocs/luci-static/resources/view/fchomo/node.js:1091 msgid "yamux" msgstr "" @@ -3325,11 +3357,11 @@ msgstr "" msgid "zashboard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:569 +#: htdocs/luci-static/resources/view/fchomo/node.js:590 msgid "zero" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1085 +#: htdocs/luci-static/resources/fchomo.js:1135 msgid "🡇" msgstr "" diff --git a/small/luci-app-fchomo/root/usr/share/fchomo/generate_client.uc b/small/luci-app-fchomo/root/usr/share/fchomo/generate_client.uc index bd0295399b..5bc52ff326 100644 --- a/small/luci-app-fchomo/root/usr/share/fchomo/generate_client.uc +++ b/small/luci-app-fchomo/root/usr/share/fchomo/generate_client.uc @@ -523,7 +523,9 @@ uci.foreach(uciconf, ucinode, (cfg) => { "padding-min": strToInt(cfg.sudoku_padding_min), "padding-max": strToInt(cfg.sudoku_padding_max), "table-type": cfg.sudoku_table_type, + "custom-tables": cfg.sudoku_custom_tables, "http-mask": (cfg.sudoku_http_mask === '0') ? false : true, + "enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null, /* Snell */ psk: cfg.snell_psk, diff --git a/small/luci-app-fchomo/root/usr/share/fchomo/generate_server.uc b/small/luci-app-fchomo/root/usr/share/fchomo/generate_server.uc index 5d6f5a9787..744188408d 100644 --- a/small/luci-app-fchomo/root/usr/share/fchomo/generate_server.uc +++ b/small/luci-app-fchomo/root/usr/share/fchomo/generate_server.uc @@ -112,7 +112,9 @@ uci.foreach(uciconf, uciserver, (cfg) => { "padding-min": strToInt(cfg.sudoku_padding_min), "padding-max": strToInt(cfg.sudoku_padding_max), "table-type": cfg.sudoku_table_type, + "custom-tables": cfg.sudoku_custom_tables, "handshake-timeout": strToInt(cfg.sudoku_handshake_timeout) ?? null, + "enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null, /* Tuic */ "congestion-controller": cfg.tuic_congestion_controller, diff --git a/small/luci-app-passwall/Makefile b/small/luci-app-passwall/Makefile index 02ecc563a7..f26e344a8a 100644 --- a/small/luci-app-passwall/Makefile +++ b/small/luci-app-passwall/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=25.12.16 +PKG_VERSION:=25.12.19 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/small/luci-app-passwall/luasrc/controller/passwall.lua b/small/luci-app-passwall/luasrc/controller/passwall.lua index 6b5ed1211f..7f373a9c72 100644 --- a/small/luci-app-passwall/luasrc/controller/passwall.lua +++ b/small/luci-app-passwall/luasrc/controller/passwall.lua @@ -502,15 +502,11 @@ function delete_select_nodes() local ids = http.formvalue("ids") local redirect = http.formvalue("redirect") string.gsub(ids, '[^' .. "," .. ']+', function(w) - if (uci:get(appname, "@global[0]", "tcp_node") or "") == w then - uci:delete(appname, '@global[0]', "tcp_node") - end - if (uci:get(appname, "@global[0]", "udp_node") or "") == w then - uci:delete(appname, '@global[0]', "udp_node") - end + local socks uci:foreach(appname, "socks", function(t) if t["node"] == w then uci:delete(appname, t[".name"]) + socks = "Socks_" .. t[".name"] end local auto_switch_node_list = uci:get(appname, t[".name"], "autoswitch_backup_node") or {} for i = #auto_switch_node_list, 1, -1 do @@ -520,16 +516,24 @@ function delete_select_nodes() end uci:set_list(appname, t[".name"], "autoswitch_backup_node", auto_switch_node_list) end) + local tcp_node = uci:get(appname, "@global[0]", "tcp_node") or "" + if tcp_node == w or tcp_node == socks then + uci:delete(appname, '@global[0]', "tcp_node") + end + local udp_node = uci:get(appname, "@global[0]", "udp_node") or "" + if udp_node == w or udp_node == socks then + uci:delete(appname, '@global[0]', "udp_node") + end uci:foreach(appname, "haproxy_config", function(t) if t["lbss"] == w then uci:delete(appname, t[".name"]) end end) uci:foreach(appname, "acl_rule", function(t) - if t["tcp_node"] == w then + if t["tcp_node"] == w or t["tcp_node"] == socks then uci:delete(appname, t[".name"], "tcp_node") end - if t["udp_node"] == w then + if t["udp_node"] == w or t["udp_node"] == socks then uci:delete(appname, t[".name"], "udp_node") end end) @@ -549,7 +553,7 @@ function delete_select_nodes() local changed = false local new_nodes = {} for _, node in ipairs(nodes) do - if node ~= w then + if node ~= w and node ~= socks then table.insert(new_nodes, node) else changed = true @@ -560,7 +564,7 @@ function delete_select_nodes() end end end - if t["fallback_node"] == w then + if t["fallback_node"] == w or t["fallback_node"] == socks then uci:delete(appname, t[".name"], "fallback_node") end end) diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index e822fb6d65..ed8796f350 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -35,6 +35,17 @@ for _, v in pairs(nodes_table) do end end +local socks_list = {} +m.uci:foreach(appname, "socks", function(s) + if s.enabled == "1" and s.node then + socks_list[#socks_list + 1] = { + id = "Socks_" .. s[".name"], + remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")), + group = "Socks" + } + end +end) + local dynamicList_write = function(self, section, value) local t = {} local t2 = {} @@ -374,9 +385,7 @@ o:value("tcp", "TCP") o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")") o:depends("dns_mode", "xray") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, ["tcp+doh"] = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "xray" then @@ -391,9 +400,7 @@ o:value("tcp", "TCP") o:value("doh", "DoH") o:depends("dns_mode", "sing-box") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, doh = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "sing-box" then @@ -489,6 +496,12 @@ o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"} local tcp = s.fields["tcp_node"] local udp = s.fields["udp_node"] +for k, v in pairs(socks_list) do + tcp:value(v.id, v["remark"]) + tcp.group[#tcp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + udp:value(v.id, v["remark"]) + udp.group[#udp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end for k, v in pairs(nodes_table) do if #normal_list == 0 then s.fields["dns_mode"]:depends({ _tcp_node_bool = "1" }) diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index de37538e08..68d884f470 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -480,9 +480,7 @@ o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")") o:depends("dns_mode", "xray") o:depends("smartdns_dns_mode", "xray") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, ["tcp+doh"] = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "xray" or s.fields["smartdns_dns_mode"]:formvalue(section) == "xray" then @@ -498,9 +496,7 @@ o:value("doh", "DoH") o:depends("dns_mode", "sing-box") o:depends("smartdns_dns_mode", "sing-box") o.cfgvalue = function(self, section) - local v = m:get(section, "v2ray_dns_mode") - local key = { udp = true, tcp = true, doh = true } - return (v and key[v]) and v or self.default + return m:get(section, "v2ray_dns_mode") end o.write = function(self, section, value) if s.fields["dns_mode"]:formvalue(section) == "sing-box" or s.fields["smartdns_dns_mode"]:formvalue(section) == "sing-box" then @@ -767,6 +763,52 @@ function s2.create(e, t) TypedSection.create(e, t) luci.http.redirect(e.extedit:format(t)) end +function s2.remove(e, t) + local socks = "Socks_" .. t + local new_node = "" + local node0 = m:get("@nodes[0]") or nil + if node0 then + new_node = node0[".name"] + end + if (m:get("@global[0]", "tcp_node") or "") == socks then + m:set('@global[0]', "tcp_node", new_node) + end + if (m:get("@global[0]", "udp_node") or "") == socks then + m:set('@global[0]', "udp_node", new_node) + end + m.uci:foreach(appname, "acl_rule", function(s) + if s["tcp_node"] and s["tcp_node"] == socks then + m:set(s[".name"], "tcp_node", "default") + end + if s["udp_node"] and s["udp_node"] == socks then + m:set(s[".name"], "udp_node", "default") + end + end) + m.uci:foreach(appname, "nodes", function(s) + local list_name = s["urltest_node"] and "urltest_node" or (s["balancing_node"] and "balancing_node") + if list_name then + local nodes = m.uci:get_list(appname, s[".name"], list_name) + if nodes then + local changed = false + local new_nodes = {} + for _, node in ipairs(nodes) do + if node ~= socks then + table.insert(new_nodes, node) + else + changed = true + end + end + if changed then + m.uci:set_list(appname, s[".name"], list_name, new_nodes) + end + end + end + if s["fallback_node"] == socks then + m:del(s[".name"], "fallback_node") + end + end) + TypedSection.remove(e, t) +end o = s2:option(DummyValue, "status", translate("Status")) o.rawhtml = true @@ -817,6 +859,12 @@ end local tcp = s.fields["tcp_node"] local udp = s.fields["udp_node"] local socks = s2.fields["node"] +for k, v in pairs(socks_list) do + tcp:value(v.id, v["remark"]) + tcp.group[#tcp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + udp:value(v.id, v["remark"]) + udp.group[#udp.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end for k, v in pairs(nodes_table) do if #normal_list == 0 then break diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua index 8c18638d3f..532783701d 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua @@ -108,6 +108,10 @@ o:depends({ [_n("protocol")] = "_balancing" }) o.widget = "checkbox" o.template = appname .. "/cbi/nodes_multivalue" o.group = {} +for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + o.group[#o.group+1] = v.group or "" +end for i, v in pairs(nodes_table) do o:value(v.id, v.remark) o.group[#o.group+1] = v.group or "" @@ -163,6 +167,10 @@ end if is_balancer then check_fallback_chain(arg[1]) end +for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end for k, v in pairs(fallback_table) do o:value(v.id, v.remark) o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index e918f1c9c2..070f755e03 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -114,6 +114,10 @@ o:depends({ [_n("protocol")] = "_urltest" }) o.widget = "checkbox" o.template = appname .. "/cbi/nodes_multivalue" o.group = {} +for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + o.group[#o.group+1] = v.group or "" +end for i, v in pairs(nodes_table) do o:value(v.id, v.remark) o.group[#o.group+1] = v.group or "" diff --git a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 8a81cdc063..b60dc9922a 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1061,12 +1061,30 @@ function gen_config(var) end end if is_new_ut_node then - local ut_node = uci:get_all(appname, ut_node_id) - local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) - if outbound then - outbound.tag = outbound.tag .. ":" .. ut_node.remarks - table.insert(outbounds, outbound) - valid_nodes[#valid_nodes + 1] = outbound.tag + local ut_node + if ut_node_id:find("Socks_") then + local socks_id = ut_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + ut_node = { + type = "sing-box", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + uot = "1", + remarks = "Socks_" .. socks_node.port + } + end + else + ut_node = uci:get_all(appname, ut_node_id) + end + if ut_node then + local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) + if outbound then + outbound.tag = outbound.tag .. ":" .. ut_node.remarks + table.insert(outbounds, outbound) + valid_nodes[#valid_nodes + 1] = outbound.tag + end end end end diff --git a/small/luci-app-passwall/luasrc/passwall/util_xray.lua b/small/luci-app-passwall/luasrc/passwall/util_xray.lua index 2680514ef8..d18a0dd996 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -753,12 +753,31 @@ function gen_config(var) end end if is_new_blc_node then - local blc_node = uci:get_all(appname, blc_node_id) - local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) - if outbound then - outbound.tag = outbound.tag .. ":" .. blc_node.remarks - table.insert(outbounds, outbound) - valid_nodes[#valid_nodes + 1] = outbound.tag + local blc_node + if blc_node_id:find("Socks_") then + local socks_id = blc_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + blc_node = { + type = "Xray", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + transport = "tcp", + stream_security = "none", + remarks = "Socks_" .. socks_node.port + } + end + else + blc_node = uci:get_all(appname, blc_node_id) + end + if blc_node then + local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) + if outbound then + outbound.tag = outbound.tag .. ":" .. blc_node.remarks + table.insert(outbounds, outbound) + valid_nodes[#valid_nodes + 1] = outbound.tag + end end end end @@ -778,17 +797,36 @@ function gen_config(var) end end if is_new_node then - local fallback_node = uci:get_all(appname, fallback_node_id) - if fallback_node.protocol ~= "_balancing" then - local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) - if outbound then - outbound.tag = outbound.tag .. ":" .. fallback_node.remarks - table.insert(outbounds, outbound) - fallback_node_tag = outbound.tag + local fallback_node + if fallback_node_id:find("Socks_") then + local socks_id = fallback_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + fallback_node = { + type = "Xray", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + transport = "tcp", + stream_security = "none", + remarks = "Socks_" .. socks_node.port + } end else - if gen_balancer(fallback_node) then - fallback_node_tag = fallback_node_id + fallback_node = uci:get_all(appname, fallback_node_id) + end + if fallback_node then + if fallback_node.protocol ~= "_balancing" then + local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run }) + if outbound then + outbound.tag = outbound.tag .. ":" .. fallback_node.remarks + table.insert(outbounds, outbound) + fallback_node_tag = outbound.tag + end + else + if gen_balancer(fallback_node) then + fallback_node_tag = fallback_node_id + end end end end diff --git a/small/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm b/small/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm index 84a04810e5..2f1429b821 100644 --- a/small/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm +++ b/small/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm @@ -216,8 +216,6 @@ table td, .table .td { let auto_detection_time = "<%=api.uci_get_type("global_other", "auto_detection_time", "0")%>" let show_node_info = "<%=api.uci_get_type("global_other", "show_node_info", "0")%>" - var node_list = []; - var ajax = { post: function(url, data, fn_success, timeout, fn_timeout) { var xhr = new XMLHttpRequest(); diff --git a/small/luci-app-passwall/root/usr/share/passwall/app.sh b/small/luci-app-passwall/root/usr/share/passwall/app.sh index 0bd0c06870..9701240f91 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/app.sh @@ -276,6 +276,13 @@ eval_unset_val() { done } +is_socks_wrap() { + case "$1" in + Socks_*) return 0 ;; + *) return 1 ;; + esac +} + ln_run() { local file_func=${1} local ln_name=${2} @@ -633,10 +640,24 @@ run_socks() { else log_file="/dev/null" fi - local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') - local remarks=$(config_n_get $node remarks) - local server_host=$(config_n_get $node address) - local server_port=$(config_n_get $node port) + + local node2socks_port=0 + local type remarks server_host server_port + if is_socks_wrap "$node"; then + node2socks_port=$(config_n_get ${node#Socks_} port 0) + fi + if [ "$node2socks_port" = "0" ]; then + type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + remarks=$(config_n_get $node remarks) + server_host=$(config_n_get $node address) + server_port=$(config_n_get $node port) + else + type="socks" + server_host="127.0.0.1" + server_port=$node2socks_port + remarks="Socks 配置($server_port 端口)" + fi + [ -n "$relay_port" ] && { server_host="127.0.0.1" server_port=$relay_port @@ -669,10 +690,16 @@ run_socks() { case "$type" in socks) - local _socks_address=$(config_n_get $node address) - local _socks_port=$(config_n_get $node port) - local _socks_username=$(config_n_get $node username) - local _socks_password=$(config_n_get $node password) + local _socks_address _socks_port _socks_username _socks_password + if [ "$node2socks_port" = "0" ]; then + _socks_address=$(config_n_get $node address) + _socks_port=$(config_n_get $node port) + _socks_username=$(config_n_get $node username) + _socks_password=$(config_n_get $node password) + else + _socks_address="127.0.0.1" + _socks_port=$node2socks_port + fi [ "$http_port" != "0" ] && { http_flag=1 config_file="${config_file//SOCKS/HTTP_SOCKS}" @@ -795,12 +822,26 @@ run_redir() { fi local proto=$(echo $proto | tr 'A-Z' 'a-z') local PROTO=$(echo $proto | tr 'a-z' 'A-Z') - local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + + local node2socks_port=0 + local type remarks server_host port + if is_socks_wrap "$node"; then + node2socks_port=$(config_n_get ${node#Socks_} port 0) + fi + if [ "$node2socks_port" = "0" ]; then + type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + remarks=$(config_n_get $node remarks) + server_host=$(config_n_get $node address) + port=$(config_n_get $node port) + else + type="socks" + server_host="127.0.0.1" + port=$node2socks_port + remarks="Socks 配置($port 端口)" + fi + local enable_log=$(config_t_get global log_${proto} 1) [ "$enable_log" != "1" ] && log_file="/dev/null" - local remarks=$(config_n_get $node remarks) - local server_host=$(config_n_get $node address) - local port=$(config_n_get $node port) [ -n "$server_host" ] && [ -n "$port" ] && { check_host $server_host [ $? != 0 ] && { @@ -814,10 +855,16 @@ run_redir() { UDP) case "$type" in socks) - local _socks_address=$(config_n_get $node address) - local _socks_port=$(config_n_get $node port) - local _socks_username=$(config_n_get $node username) - local _socks_password=$(config_n_get $node password) + local _socks_address _socks_port _socks_username _socks_password + if [ "$node2socks_port" = "0" ]; then + _socks_address=$(config_n_get $node address) + _socks_port=$(config_n_get $node port) + _socks_username=$(config_n_get $node username) + _socks_password=$(config_n_get $node password) + else + _socks_address="127.0.0.1" + _socks_port=$node2socks_port + fi run_ipt2socks flag=default proto=UDP local_port=${local_port} socks_address=${_socks_address} socks_port=${_socks_port} socks_username=${_socks_username} socks_password=${_socks_password} log_file=${log_file} ;; sing-box) @@ -898,10 +945,15 @@ run_redir() { case "$type" in socks) _socks_flag=1 - _socks_address=$(config_n_get $node address) - _socks_port=$(config_n_get $node port) - _socks_username=$(config_n_get $node username) - _socks_password=$(config_n_get $node password) + if [ "$node2socks_port" = "0" ]; then + _socks_address=$(config_n_get $node address) + _socks_port=$(config_n_get $node port) + _socks_username=$(config_n_get $node username) + _socks_password=$(config_n_get $node password) + else + _socks_address="127.0.0.1" + _socks_port=$node2socks_port + fi [ -z "$can_ipt" ] && { local _config_file=$config_file _config_file="TCP_SOCKS_${node}.json" @@ -1856,7 +1908,7 @@ acl_app() { echolog " - 全局节点未启用,跳过【${remarks}】" fi else - [ "$(config_get_type $tcp_node)" = "nodes" ] && { + [ "$(config_get_type $tcp_node)" = "nodes" ] || [ "$(config_get_type ${tcp_node#Socks_})" = "socks" ] && { if [ -n "${GLOBAL_TCP_NODE}" ] && [ "$tcp_node" = "${GLOBAL_TCP_NODE}" ]; then set_cache_var "ACL_${sid}_tcp_node" "${GLOBAL_TCP_NODE}" set_cache_var "ACL_${sid}_tcp_redir_port" "${GLOBAL_TCP_redir_port}" @@ -2015,7 +2067,7 @@ acl_app() { set_cache_var "ACL_${sid}_udp_node" "${udp_node}" set_cache_var "ACL_${sid}_udp_redir_port" "${udp_port}" else - [ "$(config_get_type $udp_node)" = "nodes" ] && { + [ "$(config_get_type $udp_node)" = "nodes" ] || [ "$(config_get_type ${udp_node#Socks_})" = "socks" ] && { if [ -n "${GLOBAL_UDP_NODE}" ] && [ "$udp_node" = "${GLOBAL_UDP_NODE}" ]; then set_cache_var "ACL_${sid}_udp_node" "${GLOBAL_UDP_NODE}" set_cache_var "ACL_${sid}_udp_redir_port" "${GLOBAL_UDP_redir_port}" @@ -2189,18 +2241,23 @@ get_config() { TCP_NODE=$(config_t_get global tcp_node) UDP_NODE=$(config_t_get global udp_node) TCP_UDP=0 - if [ "$UDP_NODE" == "tcp" ]; then + if [ "$UDP_NODE" = "tcp" ]; then UDP_NODE=$TCP_NODE TCP_UDP=1 - elif [ "$UDP_NODE" == "$TCP_NODE" ]; then + elif [ "$UDP_NODE" = "$TCP_NODE" ]; then TCP_UDP=1 fi - [ "$ENABLED" == 1 ] && { - [ -n "$TCP_NODE" ] && [ "$(config_get_type $TCP_NODE)" == "nodes" ] && ENABLED_DEFAULT_ACL=1 - [ -n "$UDP_NODE" ] && [ "$(config_get_type $UDP_NODE)" == "nodes" ] && ENABLED_DEFAULT_ACL=1 + [ "$ENABLED" = 1 ] && { + local _node + for _node in "$TCP_NODE" "$UDP_NODE"; do + [ -n "$_node" ] && case "$_node" in + Socks_*) [ "$(config_get_type "${_node#Socks_}")" = "socks" ] && ENABLED_DEFAULT_ACL=1 ;; + *) [ "$(config_get_type "$_node")" = "nodes" ] && ENABLED_DEFAULT_ACL=1 ;; + esac + done } ENABLED_ACLS=$(config_t_get global acl_enable 0) - [ "$ENABLED_ACLS" == 1 ] && { + [ "$ENABLED_ACLS" = 1 ] && { [ "$(uci show ${CONFIG} | grep "@acl_rule" | grep "enabled='1'" | wc -l)" == 0 ] && ENABLED_ACLS=0 } diff --git a/small/luci-app-passwall/root/usr/share/passwall/iptables.sh b/small/luci-app-passwall/root/usr/share/passwall/iptables.sh index f3397f0e31..90775baa12 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -256,8 +256,20 @@ load_acl() { [ -n "$(get_cache_var "ACL_${sid}_udp_node")" ] && udp_node=$(get_cache_var "ACL_${sid}_udp_node") [ -n "$(get_cache_var "ACL_${sid}_udp_redir_port")" ] && udp_port=$(get_cache_var "ACL_${sid}_udp_redir_port") [ -n "$(get_cache_var "ACL_${sid}_dns_port")" ] && dns_redirect_port=$(get_cache_var "ACL_${sid}_dns_port") - [ -n "$tcp_node" ] && tcp_node_remark=$(config_n_get $tcp_node remarks) - [ -n "$udp_node" ] && udp_node_remark=$(config_n_get $udp_node remarks) + [ -n "$tcp_node" ] && { + if is_socks_wrap "$tcp_node"; then + tcp_node_remark="Socks 配置($(config_n_get ${tcp_node#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $tcp_node remarks) + fi + } + [ -n "$udp_node" ] && { + if is_socks_wrap "$udp_node"; then + udp_node_remark="Socks 配置($(config_n_get ${udp_node#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $udp_node remarks) + fi + } use_shunt_tcp=0 use_shunt_udp=0 @@ -265,8 +277,16 @@ load_acl() { [ -n "$udp_node" ] && [ "$(config_n_get $udp_node protocol)" = "_shunt" ] && use_shunt_udp=1 [ "${use_global_config}" = "1" ] && { - tcp_node_remark=$(config_n_get $TCP_NODE remarks) - udp_node_remark=$(config_n_get $UDP_NODE remarks) + if is_socks_wrap "$TCP_NODE"; then + tcp_node_remark="Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $TCP_NODE remarks) + fi + if is_socks_wrap "$UDP_NODE"; then + udp_node_remark="Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $UDP_NODE remarks) + fi use_direct_list=${USE_DIRECT_LIST} use_proxy_list=${USE_PROXY_LIST} use_block_list=${USE_BLOCK_LIST} @@ -638,7 +658,11 @@ load_acl() { # 加载TCP默认代理模式 if [ -n "${TCP_PROXY_MODE}" ]; then [ -n "$TCP_NODE" ] && { - msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + if is_socks_wrap "$TCP_NODE"; then + msg2="${msg}使用 TCP 节点[Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)]" + else + msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + fi if [ -n "${is_tproxy}" ]; then msg2="${msg2}(TPROXY:${TCP_REDIR_PORT})" ipt_j="-j PSW_RULE" @@ -693,7 +717,11 @@ load_acl() { # 加载UDP默认代理模式 if [ -n "${UDP_PROXY_MODE}" ]; then [ -n "$UDP_NODE" -o "$TCP_UDP" = "1" ] && { - msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + if is_socks_wrap "$UDP_NODE"; then + msg2="${msg}使用 UDP 节点[Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)](TPROXY:${UDP_REDIR_PORT})" + else + msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + fi $ipt_m -A PSW $(comment "默认") -p udp -d $FAKE_IP -j PSW_RULE [ "${USE_PROXY_LIST}" = "1" ] && add_port_rules "$ipt_m -A PSW $(comment "默认") -p udp" $UDP_REDIR_PORTS "$(dst $IPSET_BLACK) -j PSW_RULE" diff --git a/small/luci-app-passwall/root/usr/share/passwall/nftables.sh b/small/luci-app-passwall/root/usr/share/passwall/nftables.sh index 99c4a8e7b6..8036d9601f 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -281,17 +281,36 @@ load_acl() { [ -n "$(get_cache_var "ACL_${sid}_udp_node")" ] && udp_node=$(get_cache_var "ACL_${sid}_udp_node") [ -n "$(get_cache_var "ACL_${sid}_udp_redir_port")" ] && udp_port=$(get_cache_var "ACL_${sid}_udp_redir_port") [ -n "$(get_cache_var "ACL_${sid}_dns_port")" ] && dns_redirect_port=$(get_cache_var "ACL_${sid}_dns_port") - [ -n "$tcp_node" ] && tcp_node_remark=$(config_n_get $tcp_node remarks) - [ -n "$udp_node" ] && udp_node_remark=$(config_n_get $udp_node remarks) - + [ -n "$tcp_node" ] && { + if is_socks_wrap "$tcp_node"; then + tcp_node_remark="Socks 配置($(config_n_get ${tcp_node#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $tcp_node remarks) + fi + } + [ -n "$udp_node" ] && { + if is_socks_wrap "$udp_node"; then + udp_node_remark="Socks 配置($(config_n_get ${udp_node#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $udp_node remarks) + fi + } use_shunt_tcp=0 use_shunt_udp=0 [ -n "$tcp_node" ] && [ "$(config_n_get $tcp_node protocol)" = "_shunt" ] && use_shunt_tcp=1 [ -n "$udp_node" ] && [ "$(config_n_get $udp_node protocol)" = "_shunt" ] && use_shunt_udp=1 [ "${use_global_config}" = "1" ] && { - tcp_node_remark=$(config_n_get $TCP_NODE remarks) - udp_node_remark=$(config_n_get $UDP_NODE remarks) + if is_socks_wrap "$TCP_NODE"; then + tcp_node_remark="Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)" + else + tcp_node_remark=$(config_n_get $TCP_NODE remarks) + fi + if is_socks_wrap "$UDP_NODE"; then + udp_node_remark="Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)" + else + udp_node_remark=$(config_n_get $UDP_NODE remarks) + fi use_direct_list=${USE_DIRECT_LIST} use_proxy_list=${USE_PROXY_LIST} use_block_list=${USE_BLOCK_LIST} @@ -660,7 +679,11 @@ load_acl() { # 加载TCP默认代理模式 if [ -n "${TCP_PROXY_MODE}" ]; then [ -n "$TCP_NODE" ] && { - msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + if is_socks_wrap "$TCP_NODE"; then + msg2="${msg}使用 TCP 节点[Socks 配置($(config_n_get ${TCP_NODE#Socks_} port) 端口)]" + else + msg2="${msg}使用 TCP 节点[$(config_n_get $TCP_NODE remarks)]" + fi if [ -n "${is_tproxy}" ]; then msg2="${msg2}(TPROXY:${TCP_REDIR_PORT})" nft_chain="PSW_MANGLE" @@ -720,7 +743,11 @@ load_acl() { # 加载UDP默认代理模式 if [ -n "${UDP_PROXY_MODE}" ]; then [ -n "$UDP_NODE" -o "$TCP_UDP" = "1" ] && { - msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + if is_socks_wrap "$UDP_NODE"; then + msg2="${msg}使用 UDP 节点[Socks 配置($(config_n_get ${UDP_NODE#Socks_} port) 端口)](TPROXY:${UDP_REDIR_PORT})" + else + msg2="${msg}使用 UDP 节点[$(config_n_get $UDP_NODE remarks)](TPROXY:${UDP_REDIR_PORT})" + fi nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW_RULE comment \"默认\"" [ "${USE_PROXY_LIST}" = "1" ] && nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") ip daddr @$NFTSET_BLACK counter jump PSW_RULE comment \"默认\"" diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index 1d387b5591..637e10a4fa 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -21,13 +21,13 @@ define Download/geoip HASH:=6878dbacfb1fcb1ee022f63ed6934bcefc95a3c4ba10c88f1131fb88dbf7c337 endef -GEOSITE_VER:=20251218102359 +GEOSITE_VER:=20251219085711 GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER) define Download/geosite URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/ URL_FILE:=dlc.dat FILE:=$(GEOSITE_FILE) - HASH:=69b16faec259d8a4a5882a78c01ad6ce3f593096c5b260c20f614664d10a213f + HASH:=400183fe3f4e74fd970536507b0245df364ba8fd1b1f128106992862ab259e9d endef GEOSITE_IRAN_VER:=202512150045 diff --git a/yt-dlp/yt_dlp/extractor/_extractors.py b/yt-dlp/yt_dlp/extractor/_extractors.py index 9129b5e68f..62d431e640 100644 --- a/yt-dlp/yt_dlp/extractor/_extractors.py +++ b/yt-dlp/yt_dlp/extractor/_extractors.py @@ -638,6 +638,7 @@ from .fc2 import ( ) from .fczenit import FczenitIE from .fifa import FifaIE +from .filmarchiv import FilmArchivIE from .filmon import ( FilmOnChannelIE, FilmOnIE, @@ -1312,12 +1313,6 @@ from .newgrounds import ( ) from .newspicks import NewsPicksIE from .newsy import NewsyIE -from .nextmedia import ( - AppleDailyIE, - NextMediaActionNewsIE, - NextMediaIE, - NextTVIE, -) from .nexx import ( NexxEmbedIE, NexxIE, diff --git a/yt-dlp/yt_dlp/extractor/dropbox.py b/yt-dlp/yt_dlp/extractor/dropbox.py index ce8435c8cb..2c1f1a40e5 100644 --- a/yt-dlp/yt_dlp/extractor/dropbox.py +++ b/yt-dlp/yt_dlp/extractor/dropbox.py @@ -14,7 +14,7 @@ from ..utils import ( class DropboxIE(InfoExtractor): - _VALID_URL = r'https?://(?:www\.)?dropbox\.com/(?:(?:e/)?scl/fi|sh?)/(?P\w+)' + _VALID_URL = r'https?://(?:www\.)?dropbox\.com/(?:(?:e/)?scl/f[io]|sh?)/(?P\w+)' _TESTS = [ { 'url': 'https://www.dropbox.com/s/nelirfsxnmcfbfh/youtube-dl%20test%20video%20%27%C3%A4%22BaW_jenozKc.mp4?dl=0', @@ -35,6 +35,9 @@ class DropboxIE(InfoExtractor): }, { 'url': 'https://www.dropbox.com/e/scl/fi/r2kd2skcy5ylbbta5y1pz/DJI_0003.MP4?dl=0&rlkey=wcdgqangn7t3lnmmv6li9mu9h', 'only_matching': True, + }, { + 'url': 'https://www.dropbox.com/scl/fo/zjfqse5txqfd7twa8iewj/AOfZzSYWUSKle2HD7XF7kzQ/A-BEAT%20C.mp4?rlkey=6tg3jkp4tv6a5vt58a6dag0mm&dl=0', + 'only_matching': True, }, ] diff --git a/yt-dlp/yt_dlp/extractor/filmarchiv.py b/yt-dlp/yt_dlp/extractor/filmarchiv.py new file mode 100644 index 0000000000..50fde2aff4 --- /dev/null +++ b/yt-dlp/yt_dlp/extractor/filmarchiv.py @@ -0,0 +1,52 @@ +from .common import InfoExtractor +from ..utils import clean_html +from ..utils.traversal import ( + find_element, + find_elements, + traverse_obj, +) + + +class FilmArchivIE(InfoExtractor): + IE_DESC = 'FILMARCHIV ON' + _VALID_URL = r'https?://(?:www\.)?filmarchiv\.at/de/filmarchiv-on/video/(?Pf_[0-9a-zA-Z]{5,})' + _TESTS = [{ + 'url': 'https://www.filmarchiv.at/de/filmarchiv-on/video/f_0305p7xKrXUPBwoNE9x6mh', + 'md5': '54a6596f6a84624531866008a77fa27a', + 'info_dict': { + 'id': 'f_0305p7xKrXUPBwoNE9x6mh', + 'ext': 'mp4', + 'title': 'Der Wurstelprater zur Kaiserzeit', + 'description': 'md5:9843f92df5cc9a4975cee7aabcf6e3b2', + 'thumbnail': r're:https://cdn\.filmarchiv\.at/f_0305/p7xKrXUPBwoNE9x6mh_v1/poster\.jpg', + }, + }, { + 'url': 'https://www.filmarchiv.at/de/filmarchiv-on/video/f_0306vI3wO0tJIsfrqYFQXF', + 'md5': '595385d7f54cb6529140ee8de7d1c3c7', + 'info_dict': { + 'id': 'f_0306vI3wO0tJIsfrqYFQXF', + 'ext': 'mp4', + 'title': 'Vor 70 Jahren: Wettgehen der Briefträger in Wien', + 'description': 'md5:b2a2e4230923cd1969d471c552e62811', + 'thumbnail': r're:https://cdn\.filmarchiv\.at/f_0306/vI3wO0tJIsfrqYFQXF_v1/poster\.jpg', + }, + }] + + def _real_extract(self, url): + media_id = self._match_id(url) + webpage = self._download_webpage(url, media_id) + path = '/'.join((media_id[:6], media_id[6:])) + formats, subtitles = self._extract_m3u8_formats_and_subtitles( + f'https://cdn.filmarchiv.at/{path}_v1_sv1/playlist.m3u8', media_id) + + return { + 'id': media_id, + 'title': traverse_obj(webpage, ({find_element(tag='title-div')}, {clean_html})), + 'description': traverse_obj(webpage, ( + {find_elements(tag='div', attr='class', value=r'.*\bborder-base-content\b', regex=True)}, ..., + {find_elements(tag='div', attr='class', value=r'.*\bprose\b', html=False, regex=True)}, ..., + {clean_html}, any)), + 'thumbnail': f'https://cdn.filmarchiv.at/{path}_v1/poster.jpg', + 'formats': formats, + 'subtitles': subtitles, + } diff --git a/yt-dlp/yt_dlp/extractor/gofile.py b/yt-dlp/yt_dlp/extractor/gofile.py index a9777a5946..e2144d7eca 100644 --- a/yt-dlp/yt_dlp/extractor/gofile.py +++ b/yt-dlp/yt_dlp/extractor/gofile.py @@ -46,6 +46,7 @@ class GofileIE(InfoExtractor): 'videopassword': 'password', }, }] + _STATIC_TOKEN = '4fd6sg89d7s6' # From https://gofile.io/dist/js/config.js _TOKEN = None def _real_initialize(self): @@ -60,13 +61,16 @@ class GofileIE(InfoExtractor): self._set_cookie('.gofile.io', 'accountToken', self._TOKEN) def _entries(self, file_id): - query_params = {'wt': '4fd6sg89d7s6'} # From https://gofile.io/dist/js/alljs.js - password = self.get_param('videopassword') - if password: + query_params = {} + if password := self.get_param('videopassword'): query_params['password'] = hashlib.sha256(password.encode()).hexdigest() + files = self._download_json( f'https://api.gofile.io/contents/{file_id}', file_id, 'Getting filelist', - query=query_params, headers={'Authorization': f'Bearer {self._TOKEN}'}) + query=query_params, headers={ + 'Authorization': f'Bearer {self._TOKEN}', + 'X-Website-Token': self._STATIC_TOKEN, + }) status = files['status'] if status == 'error-passwordRequired': diff --git a/yt-dlp/yt_dlp/extractor/netzkino.py b/yt-dlp/yt_dlp/extractor/netzkino.py index c07b1715af..05f6c23a31 100644 --- a/yt-dlp/yt_dlp/extractor/netzkino.py +++ b/yt-dlp/yt_dlp/extractor/netzkino.py @@ -2,84 +2,59 @@ from .common import InfoExtractor from ..utils import ( clean_html, int_or_none, - js_to_json, - parse_iso8601, + url_or_none, + urljoin, ) +from ..utils.traversal import traverse_obj class NetzkinoIE(InfoExtractor): - _WORKING = False - _VALID_URL = r'https?://(?:www\.)?netzkino\.de/\#!/[^/]+/(?P[^/]+)' - + _GEO_COUNTRIES = ['DE'] + _VALID_URL = r'https?://(?:www\.)?netzkino\.de/details/(?P[^/?#]+)' _TESTS = [{ - 'url': 'https://www.netzkino.de/#!/scifikino/rakete-zum-mond', - 'md5': '92a3f8b76f8d7220acce5377ea5d4873', + 'url': 'https://www.netzkino.de/details/snow-beast', + 'md5': '1a4c90fe40d3ccabce163287e45e56dd', 'info_dict': { - 'id': 'rakete-zum-mond', + 'id': 'snow-beast', 'ext': 'mp4', - 'title': 'Rakete zum Mond \u2013 Jules Verne', - 'description': 'md5:f0a8024479618ddbfa450ff48ffa6c60', - 'upload_date': '20120813', - 'thumbnail': r're:https?://.*\.jpg$', - 'timestamp': 1344858571, + 'title': 'Snow Beast', 'age_limit': 12, - }, - 'params': { - 'skip_download': 'Download only works from Germany', - }, - }, { - 'url': 'https://www.netzkino.de/#!/filme/dr-jekyll-mrs-hyde-2', - 'md5': 'c7728b2dadd04ff6727814847a51ef03', - 'info_dict': { - 'id': 'dr-jekyll-mrs-hyde-2', - 'ext': 'mp4', - 'title': 'Dr. Jekyll & Mrs. Hyde 2', - 'description': 'md5:c2e9626ebd02de0a794b95407045d186', - 'upload_date': '20190130', - 'thumbnail': r're:https?://.*\.jpg$', - 'timestamp': 1548849437, - 'age_limit': 18, - }, - 'params': { - 'skip_download': 'Download only works from Germany', + 'alt_title': 'Snow Beast', + 'cast': 'count:3', + 'categories': 'count:7', + 'creators': 'count:2', + 'description': 'md5:e604a954a7f827a80e96a3a97d48b269', + 'location': 'US', + 'release_year': 2011, + 'thumbnail': r're:https?://.+\.jpg', }, }] def _real_extract(self, url): - mobj = self._match_valid_url(url) - video_id = mobj.group('id') + video_id = self._match_id(url) + webpage = self._download_webpage(url, video_id) + next_js_data = self._search_nextjs_data(webpage, video_id) - api_url = f'https://api.netzkino.de.simplecache.net/capi-2.0a/movies/{video_id}.json?d=www' - info = self._download_json(api_url, video_id) - custom_fields = info['custom_fields'] - - production_js = self._download_webpage( - 'http://www.netzkino.de/beta/dist/production.min.js', video_id, - note='Downloading player code') - avo_js = self._search_regex( - r'var urlTemplate=(\{.*?"\})', - production_js, 'URL templates') - templates = self._parse_json( - avo_js, video_id, transform_source=js_to_json) - - suffix = { - 'hds': '.mp4/manifest.f4m', - 'hls': '.mp4/master.m3u8', - 'pmd': '.mp4', - } - film_fn = custom_fields['Streaming'][0] - formats = [{ - 'format_id': key, - 'ext': 'mp4', - 'url': tpl.replace('{}', film_fn) + suffix[key], - } for key, tpl in templates.items()] + query = traverse_obj(next_js_data, ( + 'props', '__dehydratedState', 'queries', ..., 'state', + 'data', 'data', lambda _, v: v['__typename'] == 'CmsMovie', any)) + if 'DRM' in traverse_obj(query, ('licenses', 'nodes', ..., 'properties', {str})): + self.report_drm(video_id) return { 'id': video_id, - 'formats': formats, - 'title': info['title'], - 'age_limit': int_or_none(custom_fields.get('FSK')[0]), - 'timestamp': parse_iso8601(info.get('date'), delimiter=' '), - 'description': clean_html(info.get('content')), - 'thumbnail': info.get('thumbnail'), + **traverse_obj(query, { + 'title': ('originalTitle', {clean_html}), + 'age_limit': ('fskRating', {int_or_none}), + 'alt_title': ('originalTitle', {clean_html}, filter), + 'cast': ('cast', 'nodes', ..., 'person', 'name', {clean_html}, filter), + 'creators': (('directors', 'writers'), 'nodes', ..., 'person', 'name', {clean_html}, filter), + 'categories': ('categories', 'nodes', ..., 'category', 'title', {clean_html}, filter), + 'description': ('longSynopsis', {clean_html}, filter), + 'duration': ('runtimeInSeconds', {int_or_none}), + 'location': ('productionCountry', {clean_html}, filter), + 'release_year': ('productionYear', {int_or_none}), + 'thumbnail': ('coverImage', 'masterUrl', {url_or_none}), + 'url': ('videoSource', 'pmdUrl', {urljoin('https://pmd.netzkino-seite.netzkino.de/')}), + }), } diff --git a/yt-dlp/yt_dlp/extractor/nextmedia.py b/yt-dlp/yt_dlp/extractor/nextmedia.py deleted file mode 100644 index 81da3ffde3..0000000000 --- a/yt-dlp/yt_dlp/extractor/nextmedia.py +++ /dev/null @@ -1,238 +0,0 @@ -import urllib.parse - -from .common import InfoExtractor -from ..utils import ( - clean_html, - get_element_by_class, - int_or_none, - parse_iso8601, - remove_start, - unified_timestamp, -) - - -class NextMediaIE(InfoExtractor): - IE_DESC = '蘋果日報' - _VALID_URL = r'https?://hk\.apple\.nextmedia\.com/[^/]+/[^/]+/(?P\d+)/(?P\d+)' - _TESTS = [{ - 'url': 'http://hk.apple.nextmedia.com/realtime/news/20141108/53109199', - 'md5': 'dff9fad7009311c421176d1ac90bfe4f', - 'info_dict': { - 'id': '53109199', - 'ext': 'mp4', - 'title': '【佔領金鐘】50外國領事議員撐場 讚學生勇敢香港有希望', - 'thumbnail': r're:^https?://.*\.jpg$', - 'description': 'md5:28222b9912b6665a21011b034c70fcc7', - 'timestamp': 1415456273, - 'upload_date': '20141108', - }, - }] - - _URL_PATTERN = r'\{ url: \'(.+)\' \}' - - def _real_extract(self, url): - news_id = self._match_id(url) - page = self._download_webpage(url, news_id) - return self._extract_from_nextmedia_page(news_id, url, page) - - def _extract_from_nextmedia_page(self, news_id, url, page): - redirection_url = self._search_regex( - r'window\.location\.href\s*=\s*([\'"])(?P(?!\1).+)\1', - page, 'redirection URL', default=None, group='url') - if redirection_url: - return self.url_result(urllib.parse.urljoin(url, redirection_url)) - - title = self._fetch_title(page) - video_url = self._search_regex(self._URL_PATTERN, page, 'video url') - - attrs = { - 'id': news_id, - 'title': title, - 'url': video_url, # ext can be inferred from url - 'thumbnail': self._fetch_thumbnail(page), - 'description': self._fetch_description(page), - } - - timestamp = self._fetch_timestamp(page) - if timestamp: - attrs['timestamp'] = timestamp - else: - attrs['upload_date'] = self._fetch_upload_date(url) - - return attrs - - def _fetch_title(self, page): - return self._og_search_title(page) - - def _fetch_thumbnail(self, page): - return self._og_search_thumbnail(page) - - def _fetch_timestamp(self, page): - date_created = self._search_regex('"dateCreated":"([^"]+)"', page, 'created time') - return parse_iso8601(date_created) - - def _fetch_upload_date(self, url): - return self._search_regex(self._VALID_URL, url, 'upload date', group='date') - - def _fetch_description(self, page): - return self._og_search_property('description', page) - - -class NextMediaActionNewsIE(NextMediaIE): # XXX: Do not subclass from concrete IE - IE_DESC = '蘋果日報 - 動新聞' - _VALID_URL = r'https?://hk\.dv\.nextmedia\.com/actionnews/[^/]+/(?P\d+)/(?P\d+)/\d+' - _TESTS = [{ - 'url': 'http://hk.dv.nextmedia.com/actionnews/hit/20150121/19009428/20061460', - 'md5': '05fce8ffeed7a5e00665d4b7cf0f9201', - 'info_dict': { - 'id': '19009428', - 'ext': 'mp4', - 'title': '【壹週刊】細10年男友偷食 50歲邵美琪再失戀', - 'thumbnail': r're:^https?://.*\.jpg$', - 'description': 'md5:cd802fad1f40fd9ea178c1e2af02d659', - 'timestamp': 1421791200, - 'upload_date': '20150120', - }, - }] - - def _real_extract(self, url): - news_id = self._match_id(url) - actionnews_page = self._download_webpage(url, news_id) - article_url = self._og_search_url(actionnews_page) - article_page = self._download_webpage(article_url, news_id) - return self._extract_from_nextmedia_page(news_id, url, article_page) - - -class AppleDailyIE(NextMediaIE): # XXX: Do not subclass from concrete IE - IE_DESC = '臺灣蘋果日報' - _VALID_URL = r'https?://(www|ent)\.appledaily\.com\.tw/[^/]+/[^/]+/[^/]+/(?P\d+)/(?P\d+)(/.*)?' - _TESTS = [{ - 'url': 'http://ent.appledaily.com.tw/enews/article/entertainment/20150128/36354694', - 'md5': 'a843ab23d150977cc55ef94f1e2c1e4d', - 'info_dict': { - 'id': '36354694', - 'ext': 'mp4', - 'title': '周亭羽走過摩鐵陰霾2男陪吃 九把刀孤寒看醫生', - 'thumbnail': r're:^https?://.*\.jpg$', - 'description': 'md5:2acd430e59956dc47cd7f67cb3c003f4', - 'upload_date': '20150128', - }, - }, { - 'url': 'http://www.appledaily.com.tw/realtimenews/article/strange/20150128/550549/%E4%B8%8D%E6%BB%BF%E8%A2%AB%E8%B8%A9%E8%85%B3%E3%80%80%E5%B1%B1%E6%9D%B1%E5%85%A9%E5%A4%A7%E5%AA%BD%E4%B8%80%E8%B7%AF%E6%89%93%E4%B8%8B%E8%BB%8A', - 'md5': '86b4e9132d158279c7883822d94ccc49', - 'info_dict': { - 'id': '550549', - 'ext': 'mp4', - 'title': '不滿被踩腳 山東兩大媽一路打下車', - 'thumbnail': r're:^https?://.*\.jpg$', - 'description': 'md5:175b4260c1d7c085993474217e4ab1b4', - 'upload_date': '20150128', - }, - }, { - 'url': 'http://www.appledaily.com.tw/animation/realtimenews/new/20150128/5003671', - 'md5': '03df296d95dedc2d5886debbb80cb43f', - 'info_dict': { - 'id': '5003671', - 'ext': 'mp4', - 'title': '20正妹熱舞 《刀龍傳說Online》火辣上市', - 'thumbnail': r're:^https?://.*\.jpg$', - 'description': 'md5:23c0aac567dc08c9c16a3161a2c2e3cd', - 'upload_date': '20150128', - }, - 'skip': 'redirect to http://www.appledaily.com.tw/animation/', - }, { - # No thumbnail - 'url': 'http://www.appledaily.com.tw/animation/realtimenews/new/20150128/5003673/', - 'md5': 'b06182cd386ea7bc6115ec7ff0f72aeb', - 'info_dict': { - 'id': '5003673', - 'ext': 'mp4', - 'title': '半夜尿尿 好像會看到___', - 'description': 'md5:61d2da7fe117fede148706cdb85ac066', - 'upload_date': '20150128', - }, - 'expected_warnings': [ - 'video thumbnail', - ], - 'skip': 'redirect to http://www.appledaily.com.tw/animation/', - }, { - 'url': 'http://www.appledaily.com.tw/appledaily/article/supplement/20140417/35770334/', - 'md5': 'eaa20e6b9df418c912d7f5dec2ba734d', - 'info_dict': { - 'id': '35770334', - 'ext': 'mp4', - 'title': '咖啡占卜測 XU裝熟指數', - 'thumbnail': r're:^https?://.*\.jpg$', - 'description': 'md5:7b859991a6a4fedbdf3dd3b66545c748', - 'upload_date': '20140417', - }, - }, { - 'url': 'http://www.appledaily.com.tw/actionnews/appledaily/7/20161003/960588/', - 'only_matching': True, - }, { - # Redirected from http://ent.appledaily.com.tw/enews/article/entertainment/20150128/36354694 - 'url': 'http://ent.appledaily.com.tw/section/article/headline/20150128/36354694', - 'only_matching': True, - }] - - _URL_PATTERN = r'\{url: \'(.+)\'\}' - - def _fetch_title(self, page): - return (self._html_search_regex(r'

([^<>]+)

', page, 'news title', default=None) - or self._html_search_meta('description', page, 'news title')) - - def _fetch_thumbnail(self, page): - return self._html_search_regex(r"setInitialImage\(\'([^']+)'\)", page, 'video thumbnail', fatal=False) - - def _fetch_timestamp(self, page): - return None - - def _fetch_description(self, page): - return self._html_search_meta('description', page, 'news description') - - -class NextTVIE(InfoExtractor): - _WORKING = False - _ENABLED = None # XXX: pass through to GenericIE - IE_DESC = '壹電視' - _VALID_URL = r'https?://(?:www\.)?nexttv\.com\.tw/(?:[^/]+/)+(?P\d+)' - - _TEST = { - 'url': 'http://www.nexttv.com.tw/news/realtime/politics/11779671', - 'info_dict': { - 'id': '11779671', - 'ext': 'mp4', - 'title': '「超收稅」近4千億! 藍議員籲發消費券', - 'thumbnail': r're:^https?://.*\.jpg$', - 'timestamp': 1484825400, - 'upload_date': '20170119', - 'view_count': int, - }, - } - - def _real_extract(self, url): - video_id = self._match_id(url) - - webpage = self._download_webpage(url, video_id) - - title = self._html_search_regex( - r']*>([^<]+)', webpage, 'title') - - data = self._hidden_inputs(webpage) - - video_url = data['ntt-vod-src-detailview'] - - date_str = get_element_by_class('date', webpage) - timestamp = unified_timestamp(date_str + '+0800') if date_str else None - - view_count = int_or_none(remove_start( - clean_html(get_element_by_class('click', webpage)), '點閱:')) - - return { - 'id': video_id, - 'title': title, - 'url': video_url, - 'thumbnail': data.get('ntt-vod-img-src'), - 'timestamp': timestamp, - 'view_count': view_count, - } diff --git a/yt-dlp/yt_dlp/extractor/tubitv.py b/yt-dlp/yt_dlp/extractor/tubitv.py index bb9a293b86..6db72e74b8 100644 --- a/yt-dlp/yt_dlp/extractor/tubitv.py +++ b/yt-dlp/yt_dlp/extractor/tubitv.py @@ -15,7 +15,7 @@ from ..utils import ( class TubiTvIE(InfoExtractor): IE_NAME = 'tubitv' - _VALID_URL = r'https?://(?:www\.)?tubitv\.com/(?Pvideo|movies|tv-shows)/(?P\d+)' + _VALID_URL = r'https?://(?:www\.)?tubitv\.com/(?:[a-z]{2}-[a-z]{2}/)?(?Pvideo|movies|tv-shows)/(?P\d+)' _LOGIN_URL = 'http://tubitv.com/login' _NETRC_MACHINE = 'tubitv' _TESTS = [{ @@ -73,6 +73,9 @@ class TubiTvIE(InfoExtractor): 'release_year': 1979, }, 'skip': 'Content Unavailable', + }, { + 'url': 'https://tubitv.com/es-mx/tv-shows/477363/s01-e03-jacob-dos-dos-y-la-tarjets-de-hockey-robada', + 'only_matching': True, }] # DRM formats are included only to raise appropriate error diff --git a/yt-dlp/yt_dlp/options.py b/yt-dlp/yt_dlp/options.py index 6b669d7cc0..14b582fc42 100644 --- a/yt-dlp/yt_dlp/options.py +++ b/yt-dlp/yt_dlp/options.py @@ -1212,7 +1212,7 @@ def create_parser(): help='Maximum number of seconds to sleep. Can only be used along with --min-sleep-interval') workarounds.add_option( '--sleep-subtitles', metavar='SECONDS', - dest='sleep_interval_subtitles', default=0, type=int, + dest='sleep_interval_subtitles', default=0, type=float, help='Number of seconds to sleep before each subtitle download') verbosity = optparse.OptionGroup(parser, 'Verbosity and Simulation Options')