Update On Tue Sep 16 20:39:27 CEST 2025

This commit is contained in:
github-action[bot]
2025-09-16 20:39:28 +02:00
parent ed4041c4a3
commit 2bcb715f2e
78 changed files with 2308 additions and 690 deletions

1
.github/update.log vendored
View File

@@ -1122,3 +1122,4 @@ Update On Fri Sep 12 20:36:01 CEST 2025
Update On Sat Sep 13 20:34:27 CEST 2025
Update On Sun Sep 14 20:31:36 CEST 2025
Update On Mon Sep 15 20:36:50 CEST 2025
Update On Tue Sep 16 20:39:19 CEST 2025

View File

@@ -58,7 +58,7 @@ jobs:
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.25 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }}
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.25' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
cd $(go env GOROOT)

View File

@@ -1,9 +1,8 @@
package net
import (
"context"
"io"
"net"
"runtime"
"github.com/metacubex/mihomo/common/net/deadline"
@@ -56,9 +55,37 @@ type CountFunc = network.CountFunc
var Pipe = deadline.Pipe
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
defer runtime.KeepAlive(leftConn)
defer runtime.KeepAlive(rightConn)
_ = bufio.CopyConn(context.TODO(), leftConn, rightConn)
func closeWrite(writer io.Closer) error {
if c, ok := common.Cast[network.WriteCloser](writer); ok {
return c.CloseWrite()
}
return writer.Close()
}
// Relay copies between left and right bidirectionally.
// like [bufio.CopyConn] but remove unneeded [context.Context] handle and the cost of [task.Group]
func Relay(leftConn, rightConn net.Conn) {
defer func() {
_ = leftConn.Close()
_ = rightConn.Close()
}()
ch := make(chan struct{})
go func() {
_, err := bufio.Copy(leftConn, rightConn)
if err == nil {
_ = closeWrite(leftConn)
} else {
_ = leftConn.Close()
}
close(ch)
}()
_, err := bufio.Copy(rightConn, leftConn)
if err == nil {
_ = closeWrite(rightConn)
} else {
_ = rightConn.Close()
}
<-ch
}

View File

@@ -2,8 +2,6 @@ package queue
import (
"sync"
"github.com/samber/lo"
)
// Queue is a simple concurrent safe queue
@@ -24,33 +22,32 @@ func (q *Queue[T]) Put(items ...T) {
}
// Pop returns the head of items.
func (q *Queue[T]) Pop() T {
func (q *Queue[T]) Pop() (head T) {
if len(q.items) == 0 {
return lo.Empty[T]()
return
}
q.lock.Lock()
head := q.items[0]
head = q.items[0]
q.items = q.items[1:]
q.lock.Unlock()
return head
}
// Last returns the last of item.
func (q *Queue[T]) Last() T {
func (q *Queue[T]) Last() (last T) {
if len(q.items) == 0 {
return lo.Empty[T]()
return
}
q.lock.RLock()
last := q.items[len(q.items)-1]
last = q.items[len(q.items)-1]
q.lock.RUnlock()
return last
}
// Copy get the copy of queue.
func (q *Queue[T]) Copy() []T {
items := []T{}
func (q *Queue[T]) Copy() (items []T) {
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()

View File

@@ -0,0 +1,215 @@
package queue
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestQueuePut tests the Put method of Queue
func TestQueuePut(t *testing.T) {
// Initialize a new queue
q := New[int](10)
// Test putting a single item
q.Put(1)
assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after putting one item")
// Test putting multiple items
q.Put(2, 3, 4)
assert.Equal(t, int64(4), q.Len(), "Queue length should be 4 after putting three more items")
// Test putting zero items (should not change queue)
q.Put()
assert.Equal(t, int64(4), q.Len(), "Queue length should remain unchanged when putting zero items")
}
// TestQueuePop tests the Pop method of Queue
func TestQueuePop(t *testing.T) {
// Initialize a new queue with items
q := New[int](10)
q.Put(1, 2, 3)
// Test popping items in FIFO order
item := q.Pop()
assert.Equal(t, 1, item, "First item popped should be 1")
assert.Equal(t, int64(2), q.Len(), "Queue length should be 2 after popping one item")
item = q.Pop()
assert.Equal(t, 2, item, "Second item popped should be 2")
assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after popping two items")
item = q.Pop()
assert.Equal(t, 3, item, "Third item popped should be 3")
assert.Equal(t, int64(0), q.Len(), "Queue length should be 0 after popping all items")
}
// TestQueuePopEmpty tests the Pop method on an empty queue
func TestQueuePopEmpty(t *testing.T) {
// Initialize a new empty queue
q := New[int](0)
// Test popping from an empty queue
item := q.Pop()
assert.Equal(t, 0, item, "Popping from an empty queue should return the zero value")
assert.Equal(t, int64(0), q.Len(), "Queue length should remain 0 after popping from an empty queue")
}
// TestQueueLast tests the Last method of Queue
func TestQueueLast(t *testing.T) {
// Initialize a new queue with items
q := New[int](10)
q.Put(1, 2, 3)
// Test getting the last item
item := q.Last()
assert.Equal(t, 3, item, "Last item should be 3")
assert.Equal(t, int64(3), q.Len(), "Queue length should remain unchanged after calling Last")
// Test Last on an empty queue
emptyQ := New[int](0)
emptyItem := emptyQ.Last()
assert.Equal(t, 0, emptyItem, "Last on an empty queue should return the zero value")
}
// TestQueueCopy tests the Copy method of Queue
func TestQueueCopy(t *testing.T) {
// Initialize a new queue with items
q := New[int](10)
q.Put(1, 2, 3)
// Test copying the queue
copy := q.Copy()
assert.Equal(t, 3, len(copy), "Copy should have the same number of items as the original queue")
assert.Equal(t, 1, copy[0], "First item in copy should be 1")
assert.Equal(t, 2, copy[1], "Second item in copy should be 2")
assert.Equal(t, 3, copy[2], "Third item in copy should be 3")
// Verify that modifying the copy doesn't affect the original queue
copy[0] = 99
assert.Equal(t, 1, q.Pop(), "Original queue should not be affected by modifying the copy")
}
// TestQueueLen tests the Len method of Queue
func TestQueueLen(t *testing.T) {
// Initialize a new empty queue
q := New[int](10)
assert.Equal(t, int64(0), q.Len(), "New queue should have length 0")
// Add items and check length
q.Put(1, 2)
assert.Equal(t, int64(2), q.Len(), "Queue length should be 2 after putting two items")
// Remove an item and check length
q.Pop()
assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after popping one item")
}
// TestQueueNew tests the New constructor
func TestQueueNew(t *testing.T) {
// Test creating a new queue with different hints
q1 := New[int](0)
assert.NotNil(t, q1, "New queue should not be nil")
assert.Equal(t, int64(0), q1.Len(), "New queue should have length 0")
q2 := New[int](10)
assert.NotNil(t, q2, "New queue should not be nil")
assert.Equal(t, int64(0), q2.Len(), "New queue should have length 0")
// Test with a different type
q3 := New[string](5)
assert.NotNil(t, q3, "New queue should not be nil")
assert.Equal(t, int64(0), q3.Len(), "New queue should have length 0")
}
// TestQueueConcurrency tests the concurrency safety of Queue
func TestQueueConcurrency(t *testing.T) {
// Initialize a new queue
q := New[int](100)
// Number of goroutines and operations
goroutines := 10
operations := 100
// Wait group to synchronize goroutines
wg := sync.WaitGroup{}
wg.Add(goroutines * 2) // For both producers and consumers
// Start producer goroutines
for i := 0; i < goroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < operations; j++ {
q.Put(id*operations + j)
// Small sleep to increase chance of race conditions
time.Sleep(time.Microsecond)
}
}(i)
}
// Start consumer goroutines
consumed := make(chan int, goroutines*operations)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operations; j++ {
// Try to pop an item, but don't block if queue is empty
// Use a mutex to avoid race condition between Len() check and Pop()
q.lock.Lock()
if len(q.items) > 0 {
item := q.items[0]
q.items = q.items[1:]
q.lock.Unlock()
consumed <- item
} else {
q.lock.Unlock()
}
// Small sleep to increase chance of race conditions
time.Sleep(time.Microsecond)
}
}()
}
// Wait for all goroutines to finish
wg.Wait()
// Close the consumed channel
close(consumed)
// Count the number of consumed items
consumedCount := 0
for range consumed {
consumedCount++
}
// Check that the queue is in a consistent state
totalItems := goroutines * operations
remaining := int(q.Len())
assert.Equal(t, totalItems, consumedCount+remaining, "Total items should equal consumed items plus remaining items")
}
// TestQueueWithDifferentTypes tests the Queue with different types
func TestQueueWithDifferentTypes(t *testing.T) {
// Test with string type
qString := New[string](5)
qString.Put("hello", "world")
assert.Equal(t, int64(2), qString.Len(), "Queue length should be 2")
assert.Equal(t, "hello", qString.Pop(), "First item should be 'hello'")
assert.Equal(t, "world", qString.Pop(), "Second item should be 'world'")
// Test with struct type
type Person struct {
Name string
Age int
}
qStruct := New[Person](5)
qStruct.Put(Person{Name: "Alice", Age: 30}, Person{Name: "Bob", Age: 25})
assert.Equal(t, int64(2), qStruct.Len(), "Queue length should be 2")
firstPerson := qStruct.Pop()
assert.Equal(t, "Alice", firstPerson.Name, "First person's name should be 'Alice'")
secondPerson := qStruct.Pop()
assert.Equal(t, "Bob", secondPerson.Name, "Second person's name should be 'Bob'")
}

View File

@@ -35,7 +35,7 @@ require (
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142
github.com/metacubex/utls v1.8.1-0.20250916021850-3fcad0728a32
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0

View File

@@ -139,8 +139,8 @@ github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113a
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 h1:yN3mQ4cT9sPUciw/rO0Isc/8QlO86DB6g9SEMRgQ8Cw=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 h1:csEbKOzRAxJXffOeZnnS3/kA/F55JiTbKv5jcYqCXms=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U=
github.com/metacubex/utls v1.8.1-0.20250916021850-3fcad0728a32 h1:endaN8dWxRofYpmJS46mPMQdzNyGEOwvXva42P8RY3I=
github.com/metacubex/utls v1.8.1-0.20250916021850-3fcad0728a32/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=

View File

@@ -13,7 +13,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-issue-message: 'This issue is closed because it has been stale for 5 days with no activity.'

View File

@@ -346,7 +346,7 @@ dependencies = [
"objc2-foundation 0.3.1",
"parking_lot",
"percent-encoding",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -2786,7 +2786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -4864,7 +4864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.3",
]
[[package]]
@@ -6347,9 +6347,9 @@ dependencies = [
[[package]]
name = "oxc_allocator"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90237f36cf0cd3ea2dcf9682b48fa0c1762a4b407fdcc630f60a72c277877c9f"
checksum = "9a3bc07f64a774ff9d6bd4d54d29d2a057048ed2abd5e876bffc1a295ec1b355"
dependencies = [
"allocator-api2",
"bumpalo",
@@ -6360,9 +6360,9 @@ dependencies = [
[[package]]
name = "oxc_ast"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a73ff824d44e51ac6c20381b644d48db4326935e811eeab29d52ca45e4d19670"
checksum = "f45156a623e26047f4f7e4c380621193295432ac996da090543e81f731dcdd47"
dependencies = [
"bitflags 2.9.4",
"oxc_allocator",
@@ -6377,9 +6377,9 @@ dependencies = [
[[package]]
name = "oxc_ast_macros"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f6af164ffae11248f32c449dad6a15f85d2154fe398bfec75502e0e2af5767a"
checksum = "6dce015e6ed092e7f22b2429bdebeafc52d603ad02308cc36c9599474b47d21d"
dependencies = [
"phf 0.13.1",
"proc-macro2",
@@ -6389,9 +6389,9 @@ dependencies = [
[[package]]
name = "oxc_ast_visit"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0ff52052e2cfb72fff062d4b7a393f9e9bded601dc16df796e3e460e17a9031"
checksum = "d74ea97de22e236da2350095e35fde2d390ddbb67714df11933dcc34cdd56078"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -6401,15 +6401,15 @@ dependencies = [
[[package]]
name = "oxc_data_structures"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f048c4ae3569bcc9dbbed29730b5c5f6dd3a35e9f5c3750cd4b3ed72381fbd0"
checksum = "12f7029a05de883d1461a1017211716690b342ac20c197b8539a712b426ab0a1"
[[package]]
name = "oxc_diagnostics"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d96c95294deec2f038e8c7749a751f929c263da34cc68a621472c57c916c14e"
checksum = "5c9b92b29ce9f356a8068eade68951e983247b7f5a7e51bb40047a20ca9c6ebf"
dependencies = [
"cow-utils",
"oxc-miette",
@@ -6418,9 +6418,9 @@ dependencies = [
[[package]]
name = "oxc_ecmascript"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa7b86782020722b3190c083dfc3de59cb73425d1fa275ff6f91b5b4ee509550"
checksum = "b91691a6df9a23176be896706e4b6a80b083b8af96cbb4967ec2fab804d06b4f"
dependencies = [
"cow-utils",
"num-bigint",
@@ -6433,9 +6433,9 @@ dependencies = [
[[package]]
name = "oxc_estree"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97f7078ef0c6da21657f5dcade4540c65a460d2a26a42e4418d12ecac860143a"
checksum = "8c0c1992f1cf16115e26a06761b34f9ef1460ffa1887256b85bcdc6b8bc04517"
[[package]]
name = "oxc_index"
@@ -6445,9 +6445,9 @@ checksum = "2fa07b0cfa997730afed43705766ef27792873fdf5215b1391949fec678d2392"
[[package]]
name = "oxc_parser"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef71ea1e9bde8ff15f89b60874363359fc7e9796de7bf6cdff69fa54f6869bba"
checksum = "ae29814c4bc44bb6e63bdd7386ff8664e7a3e3b21dfa78e51201d3e032b0f2b6"
dependencies = [
"bitflags 2.9.4",
"cow-utils",
@@ -6468,9 +6468,9 @@ dependencies = [
[[package]]
name = "oxc_regular_expression"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a4df17b1c47c7fe749208f3a32158dfe90dca5ce630ce86cb9415521f87eb3"
checksum = "d1270bd284948924e9c4882ae6ce70ef20d33959fd33151cdde5d89c3e4e41ce"
dependencies = [
"bitflags 2.9.4",
"oxc_allocator",
@@ -6484,9 +6484,9 @@ dependencies = [
[[package]]
name = "oxc_span"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d88265af3fb8fc2a2317144dfc40b5e120e0ebe21693cfbf7508d4d3ec6d74f"
checksum = "57176a1bafa266ae12af0207f444786a611c0fa035bf62a960ec6833a860e526"
dependencies = [
"compact_str",
"oxc-miette",
@@ -6497,9 +6497,9 @@ dependencies = [
[[package]]
name = "oxc_syntax"
version = "0.87.0"
version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2596e7891b08899f7b74a1fb87b5f5c14153918bb2966648c84581f0a7e6795"
checksum = "fcbd72aa852c40eceb8624280f64ef0d405911e073855d890f7dac114f3f36a0"
dependencies = [
"bitflags 2.9.4",
"cow-utils",
@@ -7238,7 +7238,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -7804,7 +7804,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -9435,7 +9435,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.8",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]

View File

@@ -1,7 +1,6 @@
#![feature(trait_alias)]
#![feature(auto_traits)]
#![feature(negative_impls)]
#![feature(let_chains)]
//! Boa's **boa_runtime** crate contains an example runtime and basic runtime features and
//! functionality for the `boa_engine` crate for runtime implementors.

View File

@@ -170,12 +170,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
# OXC (The Oxidation Compiler)
# We use it to parse and transpile the old script profile to esm based script profile
oxc_parser = "0.87"
oxc_allocator = "0.87"
oxc_span = "0.87"
oxc_ast = "0.87"
oxc_syntax = "0.87"
oxc_ast_visit = "0.87"
oxc_parser = "0.89"
oxc_allocator = "0.89"
oxc_span = "0.89"
oxc_ast = "0.89"
oxc_syntax = "0.89"
oxc_ast_visit = "0.89"
# Lua Integration
mlua = { version = "0.11", features = [

View File

@@ -22,6 +22,19 @@ pub struct MigrateOpts {
list: bool,
}
/// A fresh install instance should have a empty config dir,
///
/// The `app_config_dir` would create a new dir while access it.
fn is_fresh_install_instance() -> bool {
crate::utils::dirs::app_config_dir()
.ok()
.and_then(|dir| std::fs::read_dir(dir).ok())
.is_some_and(|entry| {
let dirs = entry.collect::<Vec<Result<_, _>>>();
dirs.is_empty()
})
}
pub fn parse(args: &MigrateOpts) {
let runner = if args.skip_advice {
Runner::new_with_skip_advice()
@@ -52,6 +65,14 @@ pub fn parse(args: &MigrateOpts) {
std::process::exit(1);
}
// When `Drop`, commit the changes to the migration file.
let runner = runner.drop_guard();
if is_fresh_install_instance() {
eprintln!("Fresh install detected, skip all migrations");
return;
}
if args.migration.is_none() && args.version.is_none() {
match crate::consts::BUILD_INFO.build_profile {
"Nightly" => {

View File

@@ -131,6 +131,15 @@ impl AsRef<str> for TunStack {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default, Type)]
#[serde(rename_all = "snake_case")]
pub enum BreakWhenProxyChange {
#[default]
None,
Chain,
All,
}
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, VergePatch, specta::Type)]
#[verge(patch_fn = "patch_config")]
@@ -198,9 +207,26 @@ pub struct IVerge {
/// format: {func},{key}
pub hotkeys: Option<Vec<String>>,
/// 切换代理时自动关闭连接
/// 切换代理时自动关闭连接 (已弃用)
#[deprecated(note = "use `break_when_proxy_change` instead")]
pub auto_close_connection: Option<bool>,
/// 切换代理时中断连接
/// None: 不中断
/// Chain: 仅中断使用该代理链的连接
/// All: 中断所有连接
pub break_when_proxy_change: Option<BreakWhenProxyChange>,
/// 切换配置时中断连接
/// true: 中断所有连接
/// false: 不中断连接
pub break_when_profile_change: Option<bool>,
/// 切换模式时中断连接
/// true: 中断所有连接
/// false: 不中断连接
pub break_when_mode_change: Option<bool>,
/// 默认的延迟测试连接
pub default_latency_test: Option<String>,
@@ -297,6 +323,28 @@ impl IVerge {
config.enable_service_mode = template.enable_service_mode;
}
// Handle deprecated auto_close_connection by migrating to break_when_proxy_change
if config.auto_close_connection.is_some() && config.break_when_proxy_change.is_none() {
config.break_when_proxy_change = if config.auto_close_connection.unwrap() {
Some(BreakWhenProxyChange::All)
} else {
Some(BreakWhenProxyChange::None)
};
}
// Set defaults for new options if not present
if config.break_when_proxy_change.is_none() {
config.break_when_proxy_change = template.break_when_proxy_change;
}
if config.break_when_profile_change.is_none() {
config.break_when_profile_change = template.break_when_profile_change;
}
if config.break_when_mode_change.is_none() {
config.break_when_mode_change = template.break_when_mode_change;
}
config
}
@@ -319,6 +367,9 @@ impl IVerge {
enable_proxy_guard: Some(false),
proxy_guard_interval: Some(30),
auto_close_connection: Some(true),
break_when_proxy_change: Some(BreakWhenProxyChange::All),
break_when_profile_change: Some(true),
break_when_mode_change: Some(true),
enable_builtin_enhanced: Some(true),
enable_clash_fields: Some(true),
lighten_animation_effects: Some(false),

View File

@@ -1,12 +1,11 @@
use crate::config::profile::item_type::ProfileItemType;
use crate::config::*;
use super::item::{
LocalProfileBuilder, MergeProfileBuilder, RemoteProfileBuilder, ScriptProfileBuilder,
};
use serde::{Deserialize, Deserializer, Serialize, de::Visitor};
#[derive(Debug, Serialize, specta::Type)]
#[serde(untagged)]
#[derive(Debug, serde:: Serialize, serde::Deserialize, specta::Type)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProfileBuilder {
Remote(RemoteProfileBuilder),
Local(LocalProfileBuilder),
@@ -14,74 +13,27 @@ pub enum ProfileBuilder {
Script(ScriptProfileBuilder),
}
impl<'de> Deserialize<'de> for ProfileBuilder {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ProfileBuilderVisitor;
#[derive(Debug, thiserror::Error)]
pub enum ProfileBuilderError {
#[error(transparent)]
Remote(#[from] RemoteProfileBuilderError),
#[error(transparent)]
Local(#[from] LocalProfileBuilderError),
#[error(transparent)]
Merge(#[from] MergeProfileBuilderError),
#[error(transparent)]
Script(#[from] ScriptProfileBuilderError),
}
impl<'de> Visitor<'de> for ProfileBuilderVisitor {
type Value = ProfileBuilder;
impl ProfileBuilder {
pub fn build(self) -> Result<Profile, ProfileBuilderError> {
let profile = match self {
ProfileBuilder::Remote(mut builder) => builder.build()?.into(),
ProfileBuilder::Local(builder) => builder.build()?.into(),
ProfileBuilder::Merge(builder) => builder.build()?.into(),
ProfileBuilder::Script(builder) => builder.build()?.into(),
};
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("expecting a profile builder, possible values a map with a key of `type` and a value of `remote`, `local`, `merge`, or `script`")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut mapping: serde_json::Map<String, serde_json::Value> =
serde_json::Map::new();
let mut type_field = None;
while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
if "type" == key.as_str() {
tracing::debug!("type field: {:#?}", value);
type_field =
Some(ProfileItemType::deserialize(value.clone()).map_err(|err| {
serde::de::Error::custom(format!(
"failed to deserialize profile builder type: {err}"
))
})?);
}
mapping.insert(key, value);
}
let type_field =
type_field.ok_or_else(|| serde::de::Error::missing_field("type"))?;
match type_field {
ProfileItemType::Remote => RemoteProfileBuilder::deserialize(mapping)
.map(ProfileBuilder::Remote)
.map_err(|err| {
serde::de::Error::custom(format!(
"failed to deserialize remote profile builder: {err}"
))
}),
ProfileItemType::Local => LocalProfileBuilder::deserialize(mapping)
.map(ProfileBuilder::Local)
.map_err(|err| {
serde::de::Error::custom(format!(
"failed to deserialize local profile builder: {err}"
))
}),
ProfileItemType::Merge => MergeProfileBuilder::deserialize(mapping)
.map(ProfileBuilder::Merge)
.map_err(|err| {
serde::de::Error::custom(format!(
"failed to deserialize merge profile builder: {err}"
))
}),
ProfileItemType::Script(_) => ScriptProfileBuilder::deserialize(mapping)
.map(ProfileBuilder::Script)
.map_err(|err| {
serde::de::Error::custom(format!(
"failed to deserialize script profile builder: {err}"
))
}),
}
}
}
deserializer.deserialize_map(ProfileBuilderVisitor)
Ok(profile)
}
}

View File

@@ -1,28 +1,33 @@
use super::{
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileShared, ProfileSharedBuilder,
ProfileSharedGetter, ProfileSharedSetter, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileSharedGetter, ambassador_impl_ProfileSharedSetter,
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,
ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,
};
use crate::config::{
ProfileKindGetter,
profile::item_type::{ProfileItemType, ProfileUid},
};
use crate::config::profile::item_type::ProfileUid;
use ambassador::Delegate;
use derive_builder::Builder;
use nyanpasu_macro::BuilderUpdate;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const PROFILE_TYPE: ProfileItemType = ProfileItemType::Local;
#[derive(
Default, Delegate, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type,
)]
#[builder(derive(Debug, Serialize, Deserialize, specta::Type))]
#[builder_update(patch_fn = "apply")]
#[delegate(ProfileSharedGetter, target = "shared")]
#[delegate(ProfileSharedSetter, target = "shared")]
#[delegate(ProfileMetaGetter, target = "shared")]
#[delegate(ProfileMetaSetter, target = "shared")]
#[delegate(ProfileFileIo, target = "shared")]
pub struct LocalProfile {
#[serde(flatten)]
#[builder(field(
ty = "ProfileSharedBuilder",
build = "self.shared.build().map_err(|e| LocalProfileBuilderError::from(e.to_string()))?"
build = "self.shared.build(&PROFILE_TYPE).map_err(|e| LocalProfileBuilderError::from(e.to_string()))?"
))]
#[builder_field_attr(serde(flatten))]
#[builder_update(nested)]
@@ -39,5 +44,20 @@ pub struct LocalProfile {
pub chain: Vec<ProfileUid>,
}
impl LocalProfile {
pub fn builder() -> LocalProfileBuilder {
let mut builder = LocalProfileBuilder::default();
let shared = ProfileShared::get_default_builder(&PROFILE_TYPE);
builder.shared(shared);
builder
}
}
impl ProfileKindGetter for LocalProfile {
fn kind(&self) -> ProfileItemType {
PROFILE_TYPE
}
}
impl ProfileHelper for LocalProfile {}
impl ProfileCleanup for LocalProfile {}

View File

@@ -1,31 +1,50 @@
use crate::config::{ProfileKindGetter, profile::item_type::ProfileItemType};
use super::{
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileShared, ProfileSharedBuilder,
ProfileSharedGetter, ProfileSharedSetter, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileSharedGetter, ambassador_impl_ProfileSharedSetter,
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,
ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,
};
use ambassador::Delegate;
use derive_builder::Builder;
use nyanpasu_macro::BuilderUpdate;
use serde::{Deserialize, Serialize};
const PROFILE_TYPE: ProfileItemType = ProfileItemType::Merge;
#[derive(
Default, Delegate, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type,
)]
#[builder(derive(Debug, Serialize, Deserialize, specta::Type))]
#[builder_update(patch_fn = "apply")]
#[delegate(ProfileSharedGetter, target = "shared")]
#[delegate(ProfileSharedSetter, target = "shared")]
#[delegate(ProfileMetaGetter, target = "shared")]
#[delegate(ProfileMetaSetter, target = "shared")]
#[delegate(ProfileFileIo, target = "shared")]
pub struct MergeProfile {
#[serde(flatten)]
#[builder(field(
ty = "ProfileSharedBuilder",
build = "self.shared.build().map_err(|e| MergeProfileBuilderError::from(e.to_string()))?"
build = "self.shared.build(&PROFILE_TYPE).map_err(|e| MergeProfileBuilderError::from(e.to_string()))?"
))]
#[builder_field_attr(serde(flatten))]
#[builder_update(nested)]
pub shared: ProfileShared,
}
impl MergeProfile {
pub fn builder() -> MergeProfileBuilder {
let mut builder = MergeProfileBuilder::default();
let shared = ProfileShared::get_default_builder(&PROFILE_TYPE);
builder.shared(shared);
builder
}
}
impl ProfileKindGetter for MergeProfile {
fn kind(&self) -> ProfileItemType {
PROFILE_TYPE
}
}
impl ProfileCleanup for MergeProfile {}
impl ProfileHelper for MergeProfile {}

View File

@@ -1,11 +1,9 @@
#![allow(clippy::crate_in_macro_def, dead_code)]
use super::item_type::ProfileItemType;
use crate::{enhance::ScriptType, utils::dirs};
use crate::utils::dirs;
use ambassador::{Delegate, delegatable_trait};
use anyhow::{Context, Result, bail};
use nyanpasu_macro::EnumWrapperCombined;
use serde::{Deserialize, Serialize, de::Visitor};
use serde_yaml::{Mapping, Value};
use std::{borrow::Borrow, fmt::Debug, fs, io::Write};
mod local;
@@ -26,7 +24,7 @@ pub use shared::*;
/// It is intended to be used in the default trait implementation, so it is PRIVATE.
/// NOTE: this just a setter for fields, NOT do any file operation.
#[delegatable_trait]
trait ProfileSharedSetter {
trait ProfileMetaSetter {
fn set_uid(&mut self, uid: String);
fn set_name(&mut self, name: String);
fn set_desc(&mut self, desc: Option<String>);
@@ -38,32 +36,29 @@ trait ProfileSharedSetter {
/// If access to inner data is needed, you should use the `as_xxx` or `as_mut_xxx` method to get the inner specific profile item.
#[delegatable_trait]
pub trait ProfileSharedGetter {
pub trait ProfileMetaGetter {
fn name(&self) -> &str;
fn desc(&self) -> Option<&str>;
fn kind(&self) -> &crate::config::profile::item_type::ProfileItemType;
fn uid(&self) -> &str;
fn updated(&self) -> usize;
fn file(&self) -> &str;
}
#[delegatable_trait]
pub trait ProfileKindGetter {
fn kind(&self) -> ProfileItemType;
}
/// A trait that provides some common methods for profile items
#[allow(private_bounds)]
pub trait ProfileHelper: Sized + ProfileSharedSetter + ProfileSharedGetter + Clone {
pub trait ProfileHelper:
Sized + ProfileMetaSetter + ProfileMetaGetter + ProfileKindGetter + Clone
{
async fn duplicate(&self) -> Result<Self> {
let mut duplicate_profile = self.clone();
let new_uid = utils::generate_uid(duplicate_profile.kind());
let new_file = format!(
"{}.{}",
new_uid,
match duplicate_profile.kind() {
ProfileItemType::Script(script_type) => match script_type {
ScriptType::JavaScript => "js",
ScriptType::Lua => "lua",
},
_ => "yaml",
}
);
let kind = duplicate_profile.kind();
let new_uid = utils::generate_uid(&kind);
let new_file = ProfileSharedBuilder::default_file_name(&kind, &new_uid);
let new_name = format!("{}-copy", duplicate_profile.name());
// copy file
let path = dirs::profiles_path()?;
@@ -90,89 +85,20 @@ pub trait ProfileCleanup: ProfileHelper {
}
}
#[derive(Debug, Delegate, Clone, EnumWrapperCombined, specta::Type)]
#[delegate(ProfileSharedSetter)]
#[delegate(ProfileSharedGetter)]
#[derive(
serde::Deserialize, serde::Serialize, Debug, Delegate, Clone, EnumWrapperCombined, specta::Type,
)]
#[delegate(ProfileMetaSetter)]
#[delegate(ProfileMetaGetter)]
#[delegate(ProfileKindGetter)]
#[delegate(ProfileFileIo)]
#[specta(untagged)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Profile {
Remote(RemoteProfile),
Local(LocalProfile),
Merge(MergeProfile),
Script(ScriptProfile),
}
impl Serialize for Profile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
match self {
Profile::Remote(profile) => profile.serialize(serializer),
Profile::Local(profile) => profile.serialize(serializer),
Profile::Merge(profile) => profile.serialize(serializer),
Profile::Script(profile) => profile.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Profile {
fn deserialize<D>(deserializer: D) -> Result<Profile, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct ProfileVisitor;
impl<'de> Visitor<'de> for ProfileVisitor {
type Value = Profile;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a profile")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut type_field = None;
let mut mapping = Mapping::new();
while let Some((key, value)) = map.next_entry::<String, Value>()? {
if "type" == key.as_str() {
tracing::debug!("type field: {:#?}", value);
type_field =
Some(ProfileItemType::deserialize(value.clone()).map_err(|err| {
serde::de::Error::custom(format!(
"failed to deserialize type: {err}"
))
})?);
}
mapping.insert(key.into(), value);
}
let type_field =
type_field.ok_or_else(|| serde::de::Error::missing_field("type"))?;
let other_fields = Value::Mapping(mapping);
match type_field {
ProfileItemType::Remote => RemoteProfile::deserialize(other_fields)
.map(Profile::Remote)
.map_err(serde::de::Error::custom),
ProfileItemType::Local => LocalProfile::deserialize(other_fields)
.map(Profile::Local)
.map_err(serde::de::Error::custom),
ProfileItemType::Merge => MergeProfile::deserialize(other_fields)
.map(Profile::Merge)
.map_err(serde::de::Error::custom),
ProfileItemType::Script(_) => ScriptProfile::deserialize(other_fields)
.map(Profile::Script)
.map_err(serde::de::Error::custom),
}
}
}
deserializer.deserialize_map(ProfileVisitor)
}
}
// what it actually did
// #[derive(Default, Debug, Clone, Deserialize, Serialize)]
// pub struct PrfSelected {

View File

@@ -1,2 +1,4 @@
#![allow(unused_imports)]
pub use super::{ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileSharedGetter};
pub use super::{
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileKindGetter, ProfileMetaGetter,
};

View File

@@ -1,11 +1,11 @@
use super::{
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileShared, ProfileSharedBuilder,
ProfileSharedGetter, ProfileSharedSetter, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileSharedGetter, ambassador_impl_ProfileSharedSetter,
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,
ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,
};
use crate::{
config::{
Config,
Config, ProfileKindGetter,
profile::item_type::{ProfileItemType, ProfileUid},
},
utils::{config::NyanpasuReqwestProxyExt, dirs::APP_VERSION, help},
@@ -23,6 +23,8 @@ use std::time::Duration;
use sysproxy::Sysproxy;
use url::Url;
const PROFILE_TYPE: ProfileItemType = ProfileItemType::Remote;
pub trait RemoteProfileSubscription {
async fn subscribe(&mut self, opts: Option<RemoteProfileOptionsBuilder>) -> anyhow::Result<()>;
}
@@ -31,8 +33,8 @@ pub trait RemoteProfileSubscription {
#[builder(derive(Serialize, Deserialize, Debug, specta::Type))]
#[builder(build_fn(skip, error = "RemoteProfileBuilderError"))]
#[builder_update(patch_fn = "apply")]
#[delegate(ProfileSharedSetter, target = "shared")]
#[delegate(ProfileSharedGetter, target = "shared")]
#[delegate(ProfileMetaSetter, target = "shared")]
#[delegate(ProfileMetaGetter, target = "shared")]
#[delegate(ProfileFileIo, target = "shared")]
pub struct RemoteProfile {
#[serde(flatten)]
@@ -65,6 +67,20 @@ pub struct RemoteProfile {
pub chain: Vec<ProfileUid>,
}
impl RemoteProfile {
pub fn builder() -> RemoteProfileBuilder {
let mut builder = RemoteProfileBuilder::default();
let shared = ProfileShared::get_default_builder(&PROFILE_TYPE);
builder.shared(shared);
builder
}
}
impl ProfileKindGetter for RemoteProfile {
fn kind(&self) -> ProfileItemType {
PROFILE_TYPE
}
}
impl ProfileHelper for RemoteProfile {}
impl ProfileCleanup for RemoteProfile {}
@@ -320,9 +336,7 @@ pub enum RemoteProfileBuilderError {
impl RemoteProfileBuilder {
fn default_shared(&self) -> ProfileSharedBuilder {
let mut builder = ProfileShared::builder();
builder.r#type(ProfileItemType::Remote);
builder
ProfileShared::get_default_builder(&PROFILE_TYPE)
}
fn validate(&self) -> Result<(), RemoteProfileBuilderError> {
@@ -341,7 +355,6 @@ impl RemoteProfileBuilder {
self.shared
.uid(super::utils::generate_uid(&ProfileItemType::Remote));
}
self.shared.r#type(ProfileItemType::Remote);
let url = self.url.take().unwrap();
let options = self
.option
@@ -363,7 +376,7 @@ impl RemoteProfileBuilder {
let profile = RemoteProfile {
shared: self
.shared
.build()
.build(&PROFILE_TYPE)
.map_err(|e| RemoteProfileBuilderError::Validation(e.to_string()))?,
url,
extra,
@@ -381,8 +394,9 @@ impl RemoteProfileBuilder {
Ok(profile)
}
/// NOTE: this call will block current async runtime, so it should be called in a blocking context
pub fn build(&mut self) -> Result<RemoteProfile, RemoteProfileBuilderError> {
nyanpasu_utils::runtime::block_on_current_thread(self.build_no_blocking())
nyanpasu_utils::runtime::block_on_anywhere(self.build_no_blocking())
.map_err(|e| RemoteProfileBuilderError::Validation(e.to_string()))
}
}

View File

@@ -1,9 +1,12 @@
use super::{
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileShared, ProfileSharedBuilder,
ProfileSharedGetter, ProfileSharedSetter, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileSharedGetter, ambassador_impl_ProfileSharedSetter,
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,
ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,
ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,
};
use crate::{
config::{ProfileKindGetter, profile::item_type::ProfileItemType},
enhance::ScriptType,
};
use crate::{config::profile::item_type::ProfileItemType, enhance::ScriptType};
use ambassador::Delegate;
use derive_builder::Builder;
use nyanpasu_macro::BuilderUpdate;
@@ -14,25 +17,43 @@ use serde::{Deserialize, Serialize};
)]
#[builder(derive(Debug, Serialize, Deserialize, specta::Type))]
#[builder_update(patch_fn = "apply")]
#[delegate(ProfileSharedSetter, target = "shared")]
#[delegate(ProfileSharedGetter, target = "shared")]
#[delegate(ProfileMetaSetter, target = "shared")]
#[delegate(ProfileMetaGetter, target = "shared")]
#[delegate(ProfileFileIo, target = "shared")]
pub struct ScriptProfile {
#[serde(flatten)]
#[builder(field(
ty = "ProfileSharedBuilder",
build = "self.shared.build().map_err(|e| ScriptProfileBuilderError::from(e.to_string()))?"
))]
#[builder(field(ty = "ProfileSharedBuilder", build = "self.build_shared()?"))]
#[builder_field_attr(serde(flatten))]
#[builder_update(nested)]
pub shared: ProfileShared,
pub script_type: ScriptType,
}
impl ScriptProfileBuilder {
fn build_shared(&self) -> Result<ProfileShared, ScriptProfileBuilderError> {
self.script_type
.ok_or(ScriptProfileBuilderError::UninitializedField(
"`script_type` is missing",
))
.and_then(|script_type| {
self.shared
.build(&ProfileItemType::Script(script_type))
.map_err(|e| ScriptProfileBuilderError::from(e.to_string()))
})
}
}
impl ProfileKindGetter for ScriptProfile {
fn kind(&self) -> ProfileItemType {
ProfileItemType::Script(self.script_type)
}
}
impl ScriptProfile {
pub fn builder(script_type: &ScriptType) -> ScriptProfileBuilder {
let mut builder = ScriptProfileBuilder::default();
let mut shared = ProfileSharedBuilder::default();
shared.r#type(ProfileItemType::Script(script_type.clone()));
let shared = ProfileShared::get_default_builder(&ProfileItemType::Script(*script_type));
builder.script_type(*script_type);
builder.shared(shared);
builder
}

View File

@@ -9,7 +9,7 @@ use crate::{
config::profile::item_type::ProfileItemType, enhance::ScriptType, utils::dirs::app_profiles_dir,
};
use super::{ProfileSharedGetter, ProfileSharedSetter};
use super::{ProfileMetaGetter, ProfileMetaSetter};
#[delegatable_trait]
pub trait ProfileFileIo {
@@ -24,16 +24,10 @@ pub trait ProfileFileIo {
)]
#[builder_update(patch_fn = "apply", getter)]
pub struct ProfileShared {
#[builder(default = "self.default_uid()?")]
/// Profile ID
pub uid: String,
/// profile item type
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
pub r#type: ProfileItemType,
/// profile name
#[builder(default = "self.default_name()?")]
pub name: String,
/// profile holds the file
@@ -50,6 +44,17 @@ pub struct ProfileShared {
pub updated: usize,
}
impl ProfileShared {
pub fn get_default_builder(kind: &ProfileItemType) -> ProfileSharedBuilder {
let mut builder = ProfileShared::builder();
builder
.name(ProfileSharedBuilder::default_name(kind).to_string())
.uid(ProfileSharedBuilder::default_uid(kind));
builder = builder.apply_default_file(kind).unwrap();
builder
}
}
impl ProfileFileIo for ProfileShared {
async fn read_file(&self) -> std::io::Result<String> {
let path = app_profiles_dir().map_err(std::io::Error::other)?;
@@ -71,63 +76,62 @@ impl ProfileFileIo for ProfileShared {
}
impl ProfileSharedBuilder {
fn default_uid(&self) -> Result<String, String> {
match self.r#type {
Some(ref kind) => Ok(super::utils::generate_uid(kind)),
None => Err("type should not be null".to_string()),
fn default_uid(kind: &ProfileItemType) -> String {
super::utils::generate_uid(kind)
}
pub fn default_name(kind: &ProfileItemType) -> &'static str {
match kind {
ProfileItemType::Remote => "Remote Profile",
ProfileItemType::Local => "Local Profile",
ProfileItemType::Merge => "Merge Profile",
ProfileItemType::Script(_) => "Script Profile",
}
}
fn default_name(&self) -> Result<String, String> {
match self.r#type {
Some(ProfileItemType::Remote) => Ok("Remote Profile".to_string()),
Some(ProfileItemType::Local) => Ok("Local Profile".to_string()),
Some(ProfileItemType::Merge) => Ok("Merge Profile".to_string()),
Some(ProfileItemType::Script(_)) => Ok("Script Profile".to_string()),
None => Err("type should not be null".to_string()),
pub fn default_file_name(kind: &ProfileItemType, uid: &str) -> String {
match kind {
ProfileItemType::Remote => format!("{uid}.yaml"),
ProfileItemType::Local => format!("{uid}.yaml"),
ProfileItemType::Merge => format!("{uid}.yaml"),
ProfileItemType::Script(ScriptType::JavaScript) => format!("{uid}.js"),
ProfileItemType::Script(ScriptType::Lua) => format!("{uid}.lua"),
}
}
fn default_files(&self) -> Result<String, String> {
match self.uid {
Some(ref uid) => match self.r#type {
Some(ProfileItemType::Remote) => Ok(format!("{uid}.yaml")),
Some(ProfileItemType::Local) => Ok(format!("{uid}.yaml")),
Some(ProfileItemType::Merge) => Ok(format!("{uid}.yaml")),
Some(ProfileItemType::Script(ScriptType::JavaScript)) => Ok(format!("{uid}.js")),
Some(ProfileItemType::Script(ScriptType::Lua)) => Ok(format!("{uid}.lua")),
None => Err("type should not be null".to_string()),
},
pub fn apply_default_file(
mut self,
kind: &ProfileItemType,
) -> Result<ProfileSharedBuilder, String> {
let file = match &self.uid {
Some(uid) => Ok(Self::default_file_name(kind, uid)),
None => Err("uid should not be null".to_string()),
}
}?;
self.file = Some(file);
Ok(self)
}
pub fn is_file_none(&self) -> bool {
self.file.is_none()
}
pub fn build(&self) -> Result<ProfileShared, ProfileSharedBuilderError> {
pub fn build(
&self,
kind: &ProfileItemType,
) -> Result<ProfileShared, ProfileSharedBuilderError> {
let mut builder = self.clone();
if builder.uid.is_none() {
builder.uid = Some(builder.default_uid()?);
if self.uid.is_none() {
builder.uid = Some(Self::default_uid(kind));
}
if builder.name.is_none() {
builder.name = Some(builder.default_name()?);
if self.name.is_none() {
builder.name = Some(Self::default_name(kind).to_string());
}
if builder.file.is_none() {
builder.file = Some(builder.default_files()?);
if self.file.is_none() {
builder.file = Some(Self::default_file_name(kind, builder.uid.as_ref().unwrap()));
}
Ok(ProfileShared {
uid: builder.uid.unwrap(),
r#type: match builder.r#type {
Some(ref kind) => kind.clone(),
None => {
return Err(ProfileSharedBuilderError::UninitializedField(
"type is required",
));
}
},
name: builder.name.unwrap(),
file: builder.file.unwrap(),
desc: builder.desc.clone().unwrap_or_default(),
@@ -144,7 +148,7 @@ impl ProfileShared {
}
}
impl ProfileSharedGetter for ProfileShared {
impl ProfileMetaGetter for ProfileShared {
fn name(&self) -> &str {
&self.name
}
@@ -153,10 +157,6 @@ impl ProfileSharedGetter for ProfileShared {
self.desc.as_deref()
}
fn kind(&self) -> &ProfileItemType {
&self.r#type
}
fn uid(&self) -> &str {
&self.uid
}
@@ -170,7 +170,7 @@ impl ProfileSharedGetter for ProfileShared {
}
}
impl ProfileSharedSetter for ProfileShared {
impl ProfileMetaSetter for ProfileShared {
fn set_name(&mut self, name: String) {
self.name = name;
}

View File

@@ -2,8 +2,11 @@ use crate::enhance::ScriptType;
use serde::{Deserialize, Serialize};
use strum::EnumString;
#[derive(Debug, EnumString, Clone, Serialize, Deserialize, Default, PartialEq, specta::Type)]
#[derive(
Debug, EnumString, Clone, Copy, Serialize, Deserialize, Default, PartialEq, specta::Type,
)]
#[strum(serialize_all = "snake_case")]
#[serde(tag = "kind", content = "variant", rename_all = "snake_case")]
pub enum ProfileItemType {
#[serde(rename = "remote")]
Remote,

View File

@@ -2,5 +2,9 @@ pub mod builder;
pub mod item;
pub mod item_type;
pub mod profiles;
pub use builder::ProfileBuilder;
use item::deserialize_single_or_vec;
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,286 @@
use crate::{
config::profile::{
item::{
LocalProfile, MergeProfile, Profile, RemoteProfile, RemoteProfileOptions,
ScriptProfile, SubscriptionInfo,
},
item_type::ProfileItemType,
},
enhance::ScriptType,
};
use serde_yaml;
use url::Url;
/// 测试整数类型不匹配问题
/// 这是原始问题的核心YAML 解析时整数类型可能不一致
#[test]
fn test_integer_type_mismatch_in_yaml() {
// 测试不同的整数表示形式
let yaml_with_i32 = r#"
type: remote
uid: "test-uid-1"
name: "Test Profile"
updated: 1234567890
url: "https://example.com/config.yaml"
file: sample.yaml
"#;
let yaml_with_i64 = r#"
type: remote
uid: "test-uid-2"
name: "Test Profile"
updated: 9999999999999
url: "https://example.com/config.yaml"
file: sample.yaml
"#;
let yaml_with_u64 = r#"
type: remote
uid: "test-uid-3"
name: "Test Profile"
updated: 18446744073709551615
url: "https://example.com/config.yaml"
file: sample.yaml
"#;
// 应该都能成功解析
let profile1: Result<Profile, _> = serde_yaml::from_str(yaml_with_i32);
let profile2: Result<Profile, _> = serde_yaml::from_str(yaml_with_i64);
let profile3: Result<Profile, _> = serde_yaml::from_str(yaml_with_u64);
assert!(profile1.is_ok(), "Failed to parse i32: {:?}", profile1);
assert!(profile2.is_ok(), "Failed to parse i64: {:?}", profile2);
// u64 最大值可能会被转换为 usize在 32 位系统上可能失败
if std::mem::size_of::<usize>() == 8 {
assert!(profile3.is_ok(), "Failed to parse u64: {:?}", profile3);
}
}
/// 测试 tagged enum 的正确序列化和反序列化
#[test]
fn test_tagged_enum_serialization() {
// 创建不同类型的 Profile
let remote_profile = Profile::Remote(RemoteProfile {
shared: crate::config::profile::item::ProfileShared {
uid: "remote-1".to_string(),
name: "Remote Profile".to_string(),
file: "remote-1.yaml".to_string(),
desc: Some("A remote profile".to_string()),
updated: 1234567890,
},
url: Url::parse("https://example.com/config.yaml").unwrap(),
extra: SubscriptionInfo::default(),
option: RemoteProfileOptions::default(),
chain: vec![],
});
let local_profile = Profile::Local(LocalProfile {
shared: crate::config::profile::item::ProfileShared {
uid: "local-1".to_string(),
name: "Local Profile".to_string(),
file: "local-1.yaml".to_string(),
desc: None,
updated: 1234567890,
},
symlinks: None,
chain: vec![],
});
let merge_profile = Profile::Merge(MergeProfile {
shared: crate::config::profile::item::ProfileShared {
uid: "merge-1".to_string(),
name: "Merge Profile".to_string(),
file: "merge-1.yaml".to_string(),
desc: Some("Merge multiple profiles".to_string()),
updated: 1234567890,
},
});
let script_profile = Profile::Script(ScriptProfile {
shared: crate::config::profile::item::ProfileShared {
uid: "script-1".to_string(),
name: "Script Profile".to_string(),
file: "script-1.js".to_string(),
desc: None,
updated: 1234567890,
},
script_type: ScriptType::JavaScript,
});
// 测试序列化
let remote_yaml = serde_yaml::to_string(&remote_profile).unwrap();
let local_yaml = serde_yaml::to_string(&local_profile).unwrap();
let merge_yaml = serde_yaml::to_string(&merge_profile).unwrap();
let script_yaml = serde_yaml::to_string(&script_profile).unwrap();
println!("Remote YAML:\n{}", remote_yaml);
println!("Local YAML:\n{}", local_yaml);
println!("Merge YAML:\n{}", merge_yaml);
println!("Script YAML:\n{}", script_yaml);
// 验证 YAML 包含正确的 type 标签
assert!(remote_yaml.contains("type: remote"));
assert!(local_yaml.contains("type: local"));
assert!(merge_yaml.contains("type: merge"));
assert!(script_yaml.contains("type: script"));
// 测试反序列化
let remote_parsed: Profile = serde_yaml::from_str(&remote_yaml).unwrap();
let local_parsed: Profile = serde_yaml::from_str(&local_yaml).unwrap();
let merge_parsed: Profile = serde_yaml::from_str(&merge_yaml).unwrap();
let script_parsed: Profile = serde_yaml::from_str(&script_yaml).unwrap();
// 验证反序列化后的类型正确
assert!(matches!(remote_parsed, Profile::Remote(_)));
assert!(matches!(local_parsed, Profile::Local(_)));
assert!(matches!(merge_parsed, Profile::Merge(_)));
assert!(matches!(script_parsed, Profile::Script(_)));
}
#[test]
fn test_backward_compatibility() {
// 测试新的脚本格式能被正确识别
let new_format = r#"uid: siL1cvjnvLB6
type: script
script_type: javascript
name: 花☁️处理
file: siL1cvjnvLB6.js
desc: ''
updated: 1720954186"#;
serde_yaml::from_str::<Profile>(new_format).expect("new format should works");
}
/// 测试 ProfileKindGetter trait
#[test]
fn test_profile_kind_getter() {
use crate::config::ProfileKindGetter;
let remote = RemoteProfile {
shared: Default::default(),
url: Url::parse("https://example.com").unwrap(),
extra: SubscriptionInfo::default(),
option: RemoteProfileOptions::default(),
chain: vec![],
};
assert_eq!(remote.kind(), ProfileItemType::Remote);
let local = LocalProfile {
shared: Default::default(),
symlinks: None,
chain: vec![],
};
assert_eq!(local.kind(), ProfileItemType::Local);
let merge = MergeProfile {
shared: Default::default(),
};
assert_eq!(merge.kind(), ProfileItemType::Merge);
let script_js = ScriptProfile {
shared: Default::default(),
script_type: ScriptType::JavaScript,
};
assert_eq!(
script_js.kind(),
ProfileItemType::Script(ScriptType::JavaScript)
);
}
/// 测试 builder 的默认值设置
#[tokio::test(flavor = "multi_thread")]
async fn test_builder_defaults() {
let remote_builder = RemoteProfile::builder();
let local_builder = LocalProfile::builder();
let merge_builder = MergeProfile::builder();
// let script_builder = ScriptProfile::builder(&ScriptType::JavaScript);
// 构建时应该自动填充默认值
let mut remote_builder = remote_builder;
remote_builder.url(
Url::parse(
"https://raw.githubusercontent.com/MetaCubeX/mihomo/refs/heads/Meta/docs/config.yaml",
)
.unwrap(),
);
let remote = remote_builder.build().expect("build remote profile");
assert!(!remote.shared.uid.is_empty());
assert!(!remote.shared.name.is_empty());
assert!(!remote.shared.file.is_empty());
let local = local_builder.build();
assert!(local.is_ok());
let local = local.unwrap();
assert!(!local.shared.uid.is_empty());
assert_eq!(local.shared.name, "Local Profile");
let merge = merge_builder.build();
assert!(merge.is_ok());
let merge = merge.unwrap();
assert!(!merge.shared.uid.is_empty());
assert_eq!(merge.shared.name, "Merge Profile");
}
/// 测试错误处理
#[test]
fn test_error_handling() {
// 无效的 type 值
let invalid_type = r#"
type: invalid_type
uid: "test"
name: "Test"
"#;
let result: Result<Profile, _> = serde_yaml::from_str(invalid_type);
assert!(result.is_err());
// Script 类型但缺少 script_type
let missing_script_type = r#"
type: script
uid: "script-test"
name: "Script Test"
"#;
let result: Result<Profile, _> = serde_yaml::from_str(missing_script_type);
// 应该使用默认的 script_type 或者失败
println!("Script without script_type result: {:?}", result);
// Remote 类型但缺少必需的 url 字段
let missing_url = r#"
type: remote
uid: "remote-test"
name: "Remote Test"
"#;
let result: Result<Profile, _> = serde_yaml::from_str(missing_url);
assert!(result.is_err(), "Should fail without required url field");
}
/// 测试大数字的处理
#[test]
fn test_large_numbers() {
let test_cases = vec![
(0usize, "zero"),
(1234567890usize, "normal"),
(usize::MAX, "max"),
];
for (value, desc) in test_cases {
let profile = Profile::Local(LocalProfile {
shared: crate::config::profile::item::ProfileShared {
uid: format!("test-{}", desc),
name: format!("Test {}", desc),
file: format!("test-{}.yaml", desc),
desc: None,
updated: value,
},
symlinks: None,
chain: vec![],
});
let yaml = serde_yaml::to_string(&profile).unwrap();
let parsed: Profile = serde_yaml::from_str(&yaml).unwrap();
if let Profile::Local(local) = parsed {
assert_eq!(local.shared.updated, value, "Failed for {}", desc);
} else {
panic!("Expected Local profile");
}
}
}

View File

@@ -457,6 +457,19 @@ pub fn parse_check_output(log: String) -> String {
log
}
/// DELETE /connections
/// Close all connections or a specific connection by ID
#[instrument]
pub async fn delete_connections(id: Option<&str>) -> Result<()> {
let path = match id {
Some(id) => format!("/connections/{}", id),
None => "/connections".to_string(),
};
let _ = perform_request((Method::DELETE, path.as_str())).await?;
Ok(())
}
#[test]
fn test_parse_check_output() {
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;

View File

@@ -0,0 +1,79 @@
use crate::{config::Config, core::clash::api};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Debug, Clone, Deserialize, Serialize, Type)]
pub struct ConnectionInfo {
pub id: String,
pub chains: Vec<String>,
}
/// Connection interruption service that handles closing connections based on configuration settings
pub struct ConnectionInterruptionService;
impl ConnectionInterruptionService {
/// Interrupt connections when proxy changes
pub async fn on_proxy_change() -> Result<()> {
let config = Config::verge().data().clone();
let break_when = config.break_when_proxy_change.unwrap_or_default();
match break_when {
crate::config::nyanpasu::BreakWhenProxyChange::None => {
// Do nothing
Ok(())
}
crate::config::nyanpasu::BreakWhenProxyChange::Chain => {
// TODO: Implement chain-based connection interruption
// This would require tracking which connections use which proxy chains
// For now, we'll fall back to closing all connections
api::delete_connections(None).await
}
crate::config::nyanpasu::BreakWhenProxyChange::All => {
api::delete_connections(None).await
}
}
}
/// Interrupt connections when profile changes
pub async fn on_profile_change() -> Result<()> {
let config = Config::verge().data().clone();
let break_when = config.break_when_profile_change.unwrap_or_default();
if break_when {
api::delete_connections(None).await
} else {
// Do nothing
Ok(())
}
}
/// Interrupt connections when mode changes
pub async fn on_mode_change() -> Result<()> {
let config = Config::verge().data().clone();
let break_when = config.break_when_mode_change.unwrap_or_default();
if break_when {
api::delete_connections(None).await
} else {
// Do nothing
Ok(())
}
}
/// Interrupt all connections
pub async fn interrupt_all() -> Result<()> {
api::delete_connections(None).await
}
/// Interrupt connections based on proxy chain (not yet implemented)
pub async fn interrupt_by_chain(_chain: &[String]) -> Result<()> {
// TODO: Implement chain-based connection interruption
// This would require:
// 1. Getting the current connections from the Clash API
// 2. Filtering connections that use the specified proxy chain
// 3. Closing only those connections
// For now, we'll close all connections as a fallback
api::delete_connections(None).await
}
}

View File

@@ -170,6 +170,22 @@ pub struct Runner<'a> {
store: RefCell<db::MigrationFile<'a>>,
}
pub struct DropGuard<'a>(Runner<'a>);
impl<'a> std::ops::Deref for DropGuard<'a> {
type Target = Runner<'a>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> std::ops::DerefMut for DropGuard<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Default for Runner<'_> {
fn default() -> Self {
let ver = Version::parse(crate::consts::BUILD_INFO.pkg_version).unwrap();
@@ -185,10 +201,10 @@ impl Default for Runner<'_> {
}
}
impl Drop for Runner<'_> {
impl Drop for DropGuard<'_> {
fn drop(&mut self) {
let mut store = self.store.take();
store.version = Cow::Borrowed(&self.current_version);
store.version = Cow::Borrowed(&self.0.current_version);
store.write_file().unwrap();
}
}
@@ -315,6 +331,7 @@ impl Runner<'_> {
}
Ok(())
}
pub fn run_upcoming_units(&self) -> std::io::Result<()> {
println!(
"Running all upcoming units. It is supposed to run in Nightly build. If you see this message in Stable channel, report it in Github Issues Tracker please."
@@ -331,3 +348,9 @@ impl Runner<'_> {
Ok(())
}
}
impl<'a> Runner<'a> {
pub fn drop_guard(self) -> DropGuard<'a> {
DropGuard(self)
}
}

View File

@@ -9,11 +9,14 @@ use crate::{
utils::dirs,
};
mod profile_script_newtype;
pub static UNITS: Lazy<Vec<DynMigration>> = Lazy::new(|| {
vec![
MigrateProfilesNullValue.boxed(),
MigrateLanguageOption.boxed(),
MigrateThemeSetting.boxed(),
profile_script_newtype::MigrateProfileScriptNewtype.boxed(),
]
});

View File

@@ -0,0 +1,306 @@
use std::borrow::Cow;
use semver::Version;
use serde_yaml::{
Mapping, Value,
value::{Tag, TaggedValue},
};
use crate::{core::migration::Migration, utils::help};
#[derive(Debug, Clone, Copy)]
/// 将
/// ```yaml
/// type: !script javascript
/// ```
/// 展开为
/// ```yaml
/// type: script
/// script_type: javascript
/// ```
/// 其他不做特殊处理
pub struct MigrateProfileScriptNewtype;
impl Migration<'_> for MigrateProfileScriptNewtype {
fn version(&self) -> &'static Version {
&super::VERSION
}
fn name(&self) -> Cow<'static, str> {
Cow::Borrowed("MigrateProfileScriptNewtype")
}
fn migrate(&self) -> std::io::Result<()> {
let profiles_path = crate::utils::dirs::profiles_path().map_err(std::io::Error::other)?;
if !profiles_path.exists() {
eprintln!("profiles dir not found, skipping migration");
return Ok(());
}
eprintln!("Trying to read profiles files...");
let profiles = std::fs::read_to_string(profiles_path.clone())?;
eprintln!("Trying to parse profiles files...");
let profiles: Mapping = serde_yaml::from_str(&profiles)
.map_err(|e| std::io::Error::other(format!("failed to parse profiles: {e}")))?;
eprintln!("Trying to migrate profiles files...");
let profiles = migrate_profile_data(profiles);
eprintln!("Trying to write profiles files...");
help::save_yaml(
&profiles_path,
&profiles,
Some("# Profiles Config for Clash Nyanpasu"),
)
.map_err(std::io::Error::other)?;
Ok(())
}
fn discard(&self) -> std::io::Result<()> {
let profiles_path = crate::utils::dirs::profiles_path().map_err(std::io::Error::other)?;
if !profiles_path.exists() {
eprintln!("profiles dir not found, skipping discard");
return Ok(());
}
eprintln!("Trying to read profiles files...");
let profiles = std::fs::read_to_string(profiles_path.clone())?;
eprintln!("Trying to parse profiles files...");
let profiles: Mapping = serde_yaml::from_str(&profiles)
.map_err(|e| std::io::Error::other(format!("failed to parse profiles: {e}")))?;
eprintln!("Trying to discard profiles files...");
let profiles = discard_profile_data(profiles);
eprintln!("Trying to write profiles files...");
help::save_yaml(
&profiles_path,
&profiles,
Some("# Profiles Config for Clash Nyanpasu"),
)
.map_err(std::io::Error::other)?;
Ok(())
}
}
fn migrate_profile_data(mut mapping: serde_yaml::Mapping) -> serde_yaml::Mapping {
// We just need to iter items
if let Some(items) = mapping.get_mut("items")
&& let Some(items) = items.as_sequence_mut()
{
for item in items {
if let Some(item) = item.as_mapping_mut()
&& let Some(ty) = item.get("type").cloned()
&& let Value::Tagged(tag) = ty
&& tag.tag == "script"
&& let Some(script_kind) = tag.value.as_str()
{
item.insert(
"type".into(),
serde_yaml::Value::String("script".to_string()),
);
item.insert(
"script_type".into(),
serde_yaml::Value::String(script_kind.to_string()),
);
}
}
}
mapping
}
fn discard_profile_data(mut mapping: serde_yaml::Mapping) -> serde_yaml::Mapping {
// We just need to iter items
if let Some(items) = mapping.get_mut("items")
&& let Some(items) = items.as_sequence_mut()
{
for item in items {
if let Some(item) = item.as_mapping_mut()
&& let Some(ty) = item.get("type").cloned()
&& let Value::String(ty) = ty
&& ty == "script"
&& let Some(script_kind) = item.get("script_type").cloned()
{
item.insert(
"type".into(),
serde_yaml::Value::Tagged(Box::new(TaggedValue {
tag: Tag::new("script"),
value: script_kind,
})),
);
item.remove("script_type");
}
}
}
mapping
}
#[cfg(test)]
mod tests {
use crate::config::Profiles;
use super::*;
use pretty_assertions::assert_str_eq;
const ORIGINAL_SAMPLE: &str = r#"current:
- rIWXPHuafvEM
chain: []
valid:
- dns
- unified-delay
- tcp-concurrent
- tun
- profile
items:
- uid: rIWXPHuafvEM
type: remote
name: 🌸云
file: rIWXPHuafvEM.yaml
desc: null
updated: 1758110672
url: https://example.com
extra:
upload: 3641183914
download: 39111158992
total: 42946719600
expire: 1769123200
option:
with_proxy: false
self_proxy: true
update_interval: 1440
chain:
- siL1cvjnvLB6
- sxI0dHKeqSNg
- uid: siL1cvjnvLB6
type: !script javascript
name: 花☁️处理
file: siL1cvjnvLB6.js
desc: ''
updated: 1720954186
- uid: sxI0dHKeqSNg
type: !script javascript
name: 🌸☁️图标
file: sxI0dHKeqSNg.js
desc: ''
updated: 1722656540
- uid: sZYZe33w7RKV
type: !script lua
name: 图标
file: sZYZe33w7RKV.lua
desc: ''
updated: 1724082226
- uid: lkvV5JXfzO34
type: local
name: New Profile
file: lkvV5JXfzO34.yaml
desc: ''
updated: 1725587682
chain: []
- uid: lJynXCoMMIUd
type: local
name: New Profile
file: lJynXCoMMIUd.yaml
desc: ''
updated: 1726252304
chain: []
- uid: lBtaVEaMAR97
type: local
name: Test
file: lBtaVEaMAR97.yaml
desc: ''
updated: 1727621893
chain: []
"#;
const MIGRATED_SAMPLE: &str = r#"current:
- rIWXPHuafvEM
chain: []
valid:
- dns
- unified-delay
- tcp-concurrent
- tun
- profile
items:
- uid: rIWXPHuafvEM
type: remote
name: 🌸云
file: rIWXPHuafvEM.yaml
desc: null
updated: 1758110672
url: https://example.com
extra:
upload: 3641183914
download: 39111158992
total: 42946719600
expire: 1769123200
option:
with_proxy: false
self_proxy: true
update_interval: 1440
chain:
- siL1cvjnvLB6
- sxI0dHKeqSNg
- uid: siL1cvjnvLB6
type: script
name: 花☁️处理
file: siL1cvjnvLB6.js
desc: ''
updated: 1720954186
script_type: javascript
- uid: sxI0dHKeqSNg
type: script
name: 🌸☁️图标
file: sxI0dHKeqSNg.js
desc: ''
updated: 1722656540
script_type: javascript
- uid: sZYZe33w7RKV
type: script
name: 图标
file: sZYZe33w7RKV.lua
desc: ''
updated: 1724082226
script_type: lua
- uid: lkvV5JXfzO34
type: local
name: New Profile
file: lkvV5JXfzO34.yaml
desc: ''
updated: 1725587682
chain: []
- uid: lJynXCoMMIUd
type: local
name: New Profile
file: lJynXCoMMIUd.yaml
desc: ''
updated: 1726252304
chain: []
- uid: lBtaVEaMAR97
type: local
name: Test
file: lBtaVEaMAR97.yaml
desc: ''
updated: 1727621893
chain: []
"#;
#[test]
fn test_migrate_existing_data() {
let original_data = serde_yaml::from_str::<serde_yaml::Mapping>(ORIGINAL_SAMPLE).unwrap();
let migrated_data = migrate_profile_data(original_data);
let output_data = serde_yaml::to_string(&migrated_data).unwrap();
assert_str_eq!(output_data, MIGRATED_SAMPLE);
}
#[test]
fn test_discard_existing_data() {
let migrated_data = serde_yaml::from_str::<serde_yaml::Mapping>(MIGRATED_SAMPLE).unwrap();
let original_data = discard_profile_data(migrated_data);
let output_data = serde_yaml::to_string(&original_data).unwrap();
assert_str_eq!(output_data, ORIGINAL_SAMPLE);
}
#[test]
#[ignore]
fn test_profile_parse_migrated_data() {
let profiles = serde_yaml::from_str::<Profiles>(MIGRATED_SAMPLE).unwrap();
eprintln!("{profiles:#?}");
}
}

View File

@@ -1,15 +1,17 @@
pub mod clash;
pub mod connection_interruption;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod manager;
pub mod service;
pub mod storage;
pub mod sysopt;
pub mod tasks;
pub mod tray;
pub mod updater;
#[cfg(windows)]
pub mod win_uwp;
pub use self::clash::core::*;
pub mod migration;
pub mod service;
pub mod state;

View File

@@ -7,6 +7,9 @@ use std::sync::Arc;
use sysproxy::Sysproxy;
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
#[cfg(target_os = "linux")]
use std::process::Command;
pub struct Sysopt {
/// current system proxy setting
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
@@ -30,6 +33,25 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172
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,<local>";
#[cfg(target_os = "linux")]
fn detect_desktop_environment() -> String {
std::env::var("XDG_CURRENT_DESKTOP")
.or_else(|_| std::env::var("DESKTOP_SESSION"))
.unwrap_or_else(|_| "unknown".to_string())
.to_lowercase()
}
#[cfg(target_os = "linux")]
fn get_autostart_requirements(desktop_env: &str) -> (bool, Vec<String>) {
match desktop_env {
"kde" | "plasma" => {
// KDE 可能需要特殊的桌面文件格式或权限
(true, vec!["X-KDE-autostart-after=panel".to_string()])
}
_ => (false, vec![]),
}
}
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
@@ -145,8 +167,12 @@ impl Sysopt {
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
log::info!(target: "app", "Initializing auto-launch with enable={}", enable);
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
log::debug!(target: "app", "Resolved app executable path: {:?}", app_exe);
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
@@ -158,9 +184,13 @@ impl Sysopt {
.ok_or(anyhow!("failed to get app_path"))?
.to_string();
log::debug!(target: "app", "Initial app path: {}", app_path);
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
#[cfg(target_os = "windows")]
log::debug!(target: "app", "Windows formatted app path: {}", app_path);
// use the /Applications/Clash Nyanpasu.app path
#[cfg(target_os = "macos")]
@@ -174,6 +204,8 @@ impl Sysopt {
}
})()
.unwrap_or(app_path);
#[cfg(target_os = "macos")]
log::debug!(target: "app", "macOS app path: {}", app_path);
// fix #403
#[cfg(target_os = "linux")]
@@ -182,25 +214,37 @@ impl Sysopt {
use tauri::Manager;
let handle = Handle::global();
match handle.app_handle.lock().as_ref() {
let appimage_path = match handle.app_handle.lock().as_ref() {
Some(app_handle) => {
// 优先使用 Tauri 环境变量
let appimage = app_handle.env().appimage;
appimage
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or(app_path)
appimage.and_then(|p| p.to_str().map(|s| s.to_string()))
}
None => app_path,
}
None => None,
};
// 备用方法:检查环境变量
let fallback_appimage = std::env::var("APPIMAGE").ok();
let final_path = appimage_path.or(fallback_appimage).unwrap_or(app_path);
log::info!(target: "app", "Using executable path for auto-launch: {}", final_path);
final_path
};
log::info!(target: "app", "Using executable path for auto-launch: {}", app_path);
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()?;
log::debug!(target: "app", "AutoLaunch builder created with app_name: {}", app_name);
// 避免在开发时将自启动关了
#[cfg(feature = "verge-dev")]
if !enable {
log::info!(target: "app", "Skipping auto-launch setup in development mode");
return Ok(());
}
@@ -208,16 +252,25 @@ impl Sysopt {
{
if enable && !auto.is_enabled().unwrap_or(false) {
// 避免重复设置登录项
log::debug!(target: "app", "macOS: Disabling existing auto-launch");
let _ = auto.disable();
log::debug!(target: "app", "macOS: Enabling auto-launch");
auto.enable()?;
} else if !enable {
log::debug!(target: "app", "macOS: Disabling auto-launch");
let _ = auto.disable();
}
}
#[cfg(not(target_os = "macos"))]
if enable {
auto.enable()?;
{
if enable {
log::debug!(target: "app", "Enabling auto-launch for non-macOS platform");
auto.enable()?;
} else {
log::debug!(target: "app", "Disabling auto-launch for non-macOS platform");
let _ = auto.disable();
}
}
*self.auto_launch.lock() = Some(auto);

View File

@@ -3,7 +3,7 @@ use super::super::{
task::{Task, TaskID, TaskManager, TaskSchedule},
};
use crate::{
config::{Config, ProfileSharedGetter},
config::{Config, ProfileMetaGetter},
feat,
};
use anyhow::Result;

View File

@@ -118,7 +118,17 @@ pub enum ChainType {
}
#[derive(
Debug, EnumString, Clone, Serialize, Deserialize, Default, Eq, PartialEq, Hash, specta::Type,
Debug,
EnumString,
Clone,
Copy,
Serialize,
Deserialize,
Default,
Eq,
PartialEq,
Hash,
specta::Type,
)]
#[strum(serialize_all = "snake_case")]
pub enum ScriptType {

View File

@@ -8,7 +8,7 @@ mod utils;
pub use self::chain::ScriptType;
use self::{chain::*, field::*, merge::*, script::*, tun::*};
use crate::config::{Config, ProfileSharedGetter, nyanpasu::ClashCore};
use crate::config::{Config, ProfileMetaGetter, nyanpasu::ClashCore};
pub use chain::PostProcessingOutput;
use futures::future::join_all;
use indexmap::IndexMap;

View File

@@ -94,6 +94,13 @@ pub fn change_clash_mode(mode: String) {
// refresh proxies
update_proxies_buff(Some(rx));
// Interrupt connections based on configuration
tauri::async_runtime::spawn(async move {
let _ =
crate::core::connection_interruption::ConnectionInterruptionService::on_mode_change()
.await;
});
}
// 切换系统代理

View File

@@ -1,6 +1,9 @@
use crate::{
config::{profile::ProfileBuilder, *},
core::{storage::Storage, tasks::jobs::ProfilesJobGuard, updater::ManifestVersionLatest, *},
core::{
logger::Logger, storage::Storage, tasks::jobs::ProfilesJobGuard,
updater::ManifestVersionLatest, *,
},
enhance::PostProcessingOutput,
feat,
utils::{
@@ -264,6 +267,10 @@ pub async fn patch_profiles_config(profiles: ProfilesBuilder) -> Result {
handle::Handle::refresh_clash();
Config::profiles().apply();
(Config::profiles().data().save_file())?;
// Interrupt connections based on configuration
let _ = crate::core::connection_interruption::ConnectionInterruptionService::on_profile_change().await;
Ok(())
}
Err(err) => {
@@ -509,7 +516,7 @@ pub fn get_sys_proxy() -> Result<GetSysProxyResponse> {
#[tauri::command]
#[specta::specta]
pub fn get_clash_logs() -> Result<VecDeque<String>> {
Ok(logger::Logger::global().get_log())
Ok(Logger::global().get_log())
}
#[tauri::command]
@@ -685,6 +692,11 @@ pub async fn mutate_proxies() -> Result<crate::core::clash::proxies::Proxies> {
pub async fn select_proxy(group: String, name: String) -> Result<()> {
use crate::core::clash::proxies::{ProxiesGuard, ProxiesGuardExt};
(ProxiesGuard::global().select_proxy(&group, &name).await)?;
// Interrupt connections based on configuration
let _ = crate::core::connection_interruption::ConnectionInterruptionService::on_proxy_change()
.await;
Ok(())
}

View File

@@ -1,4 +1,4 @@
#![feature(auto_traits, negative_impls, let_chains, trait_alias)]
#![feature(auto_traits, negative_impls, trait_alias)]
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
@@ -110,6 +110,11 @@ pub fn run() -> std::io::Result<()> {
.is_ok_and(|instance| instance.is_some())
&& let Err(e) = init::run_pending_migrations()
{
// Try to open migration log files
if let Ok(data_dir) = crate::utils::dirs::app_data_dir() {
let _ = crate::utils::open::that(data_dir.join("migration.log"));
}
utils::dialog::panic_dialog(&format!(
"Failed to finish migration event: {e}\nYou can see the detailed information at migration.log in your local data dir.\nYou're supposed to submit it as the attachment of new issue.",
));

View File

@@ -742,6 +742,7 @@ export const events = __makeEvents__<{
/** user-defined types **/
export type BreakWhenProxyChange = 'none' | 'chain' | 'all'
export type BuildInfo = {
app_name: string
app_version: string
@@ -939,9 +940,29 @@ export type IVerge = {
*/
hotkeys: string[] | null
/**
* 切换代理时自动关闭连接
* 切换代理时自动关闭连接 (已弃用)
* @deprecated use `break_when_proxy_change` instead
*/
auto_close_connection: boolean | null
/**
* 切换代理时中断连接
* None: 不中断
* Chain: 仅中断使用该代理链的连接
* All: 中断所有连接
*/
break_when_proxy_change: BreakWhenProxyChange | null
/**
* 切换配置时中断连接
* true: 中断所有连接
* false: 不中断连接
*/
break_when_profile_change: boolean | null
/**
* 切换模式时中断连接
* true: 中断所有连接
* false: 不中断连接
*/
break_when_mode_change: boolean | null
/**
* 默认的延迟测试连接
*/
@@ -1013,12 +1034,10 @@ export type JsonValue =
| JsonValue[]
| Partial<{ [key in string]: JsonValue }>
export type LocalProfile = {
uid: string
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType
uid: string
/**
* profile name
*/
@@ -1050,12 +1069,10 @@ export type LocalProfile = {
*
*/
export type LocalProfileBuilder = {
uid: string | null
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType | null
uid: string | null
/**
* profile name
*/
@@ -1098,12 +1115,10 @@ export type ManifestVersionLatest = {
clash_premium: string
}
export type MergeProfile = {
uid: string
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType
uid: string
/**
* profile name
*/
@@ -1126,12 +1141,10 @@ export type MergeProfile = {
*
*/
export type MergeProfileBuilder = {
uid: string | null
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType | null
uid: string | null
/**
* profile name
*/
@@ -1178,20 +1191,15 @@ export type PostProcessingOutput = {
advice: [LogSpan, string][]
}
export type Profile =
| RemoteProfile
| LocalProfile
| MergeProfile
| ScriptProfile
| ({ type: 'remote' } & RemoteProfile)
| ({ type: 'local' } & LocalProfile)
| ({ type: 'merge' } & MergeProfile)
| ({ type: 'script' } & ScriptProfile)
export type ProfileBuilder =
| RemoteProfileBuilder
| LocalProfileBuilder
| MergeProfileBuilder
| ScriptProfileBuilder
export type ProfileItemType =
| 'remote'
| 'local'
| { script: ScriptType }
| 'merge'
| ({ type: 'remote' } & RemoteProfileBuilder)
| ({ type: 'local' } & LocalProfileBuilder)
| ({ type: 'merge' } & MergeProfileBuilder)
| ({ type: 'script' } & ScriptProfileBuilder)
/**
* Define the `profiles.yaml` schema
*/
@@ -1273,12 +1281,10 @@ export type ProxyItem = {
}
export type ProxyItemHistory = { time: string; delay: number }
export type RemoteProfile = {
uid: string
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType
uid: string
/**
* profile name
*/
@@ -1318,12 +1324,10 @@ export type RemoteProfile = {
*
*/
export type RemoteProfileBuilder = {
uid: string | null
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType | null
uid: string | null
/**
* profile name
*/
@@ -1420,12 +1424,10 @@ export type RuntimeInfos = {
nyanpasu_data_dir: string
}
export type ScriptProfile = {
uid: string
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType
uid: string
/**
* profile name
*/
@@ -1442,18 +1444,16 @@ export type ScriptProfile = {
* update time
*/
updated: number
}
} & { script_type: ScriptType }
/**
* Builder for [`ScriptProfile`](struct.ScriptProfile.html).
*
*/
export type ScriptProfileBuilder = {
uid: string | null
/**
* profile item type
* enum value: remote | local | script | merge
* Profile ID
*/
type: ProfileItemType | null
uid: string | null
/**
* profile name
*/
@@ -1470,7 +1470,7 @@ export type ScriptProfileBuilder = {
* update time
*/
updated: number | null
}
} & { script_type: ScriptType | null }
export type ScriptType = 'javascript' | 'lua'
export type ServiceStatus = 'not_installed' | 'stopped' | 'running'
export type StatisticWidgetVariant = 'large' | 'small'

View File

@@ -52,6 +52,13 @@ export interface VergeConfig {
always_on_top?: boolean
}
export interface AutoReloadConfig {
enabled: boolean
onProxyChange: boolean
onProfileChange: boolean
onModeChange: boolean
}
export interface SystemProxy {
enable: boolean
server: string

View File

@@ -0,0 +1 @@
.tanstack

View File

@@ -31,7 +31,7 @@
"country-code-emoji": "2.3.0",
"country-emoji": "1.5.6",
"dayjs": "1.11.18",
"framer-motion": "12.23.12",
"framer-motion": "12.23.13",
"i18next": "25.5.2",
"jotai": "2.14.0",
"json-schema": "0.4.0",
@@ -59,9 +59,9 @@
"@iconify/json": "2.2.384",
"@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.87.4",
"@tanstack/react-router": "1.131.41",
"@tanstack/react-router-devtools": "1.131.42",
"@tanstack/router-plugin": "1.131.43",
"@tanstack/react-router": "1.131.44",
"@tanstack/react-router-devtools": "1.131.44",
"@tanstack/router-plugin": "1.131.44",
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
"@tauri-apps/plugin-dialog": "2.4.0",
"@tauri-apps/plugin-fs": "2.4.2",

View File

@@ -1,10 +1,7 @@
import { Box } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
import { getLanguage, ProfileType } from '../utils'
export const LanguageChip = ({ type }: { type: ProfileType }) => {
const lang = getLanguage(type, true)
export const LanguageChip = ({ lang }: { lang: string }) => {
return (
lang && (
<Box

View File

@@ -9,7 +9,7 @@ import { Add } from '@mui/icons-material'
import { ListItemButton } from '@mui/material'
import { ProfileQueryResultItem, useProfile } from '@nyanpasu/interface'
import { alpha } from '@nyanpasu/ui'
import { ClashProfile, filterProfiles } from '../utils'
import { ClashProfile, ClashProfileBuilder, filterProfiles } from '../utils'
import ChainItem from './chain-item'
import { atomChainsSelected, atomGlobalChainCurrent } from './store'
@@ -53,7 +53,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
await patch.mutateAsync({
uid: currentProfile.uid,
profile: {
...currentProfile,
...(currentProfile as ClashProfileBuilder),
chain: updatedChains,
},
})

View File

@@ -24,6 +24,7 @@ import { message } from '@/utils/notification'
import { Divider, InputAdornment } from '@mui/material'
import {
LocalProfile,
ProfileBuilder,
ProfileQueryResultItem,
ProfileTemplate,
RemoteProfile,
@@ -33,6 +34,7 @@ import {
import { BaseDialog } from '@nyanpasu/ui'
import { LabelSwitch } from '../setting/modules/clash-field'
import { ReadProfile } from './read-profile'
import { ClashProfile, ClashProfileBuilder } from './utils'
const ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))
@@ -67,21 +69,20 @@ export const ProfileDialog = ({
const addProfileCtx = use(AddProfileContext)
const [localProfileMessage, setLocalProfileMessage] = useState('')
const { control, watch, handleSubmit, reset, setValue } = useForm<
RemoteProfile | LocalProfile
>({
defaultValues: profile || {
type: 'remote',
name: addProfileCtx?.name || t(`New Profile`),
desc: addProfileCtx?.desc || '',
url: addProfileCtx?.url || '',
option: {
// user_agent: "",
with_proxy: false,
self_proxy: false,
const { control, watch, handleSubmit, reset, setValue } =
useForm<ClashProfileBuilder>({
defaultValues: (profile as ClashProfile) || {
type: 'remote',
name: addProfileCtx?.name || t(`New Profile`),
desc: addProfileCtx?.desc || '',
url: addProfileCtx?.url || '',
option: {
// user_agent: "",
with_proxy: false,
self_proxy: false,
},
},
},
})
})
useEffect(() => {
if (addProfileCtx) {
@@ -126,7 +127,6 @@ export const ProfileDialog = ({
editorMarks.current.length > 0 &&
editorMarks.current.some((m) => m.severity === 8)
// eslint-disable-next-line react-compiler/react-compiler
const onSubmit = handleSubmit(async (form) => {
if (editorHasError()) {
message('Please fix the error before saving', {
@@ -180,7 +180,7 @@ export const ProfileDialog = ({
await contentFn.upsert.mutateAsync(value)
await patch.mutateAsync({
uid: form.uid,
uid: form.uid!,
profile: form,
})
}
@@ -336,7 +336,7 @@ export const ProfileDialog = ({
useAsyncEffect(async () => {
if (profile) {
reset(profile)
reset(profile as ClashProfileBuilder)
}
if (isEdit) {

View File

@@ -35,6 +35,7 @@ import {
import { alpha, cleanDeepClickEvent, cn } from '@nyanpasu/ui'
import { ProfileDialog } from './profile-dialog'
import { GlobalUpdatePendingContext } from './provider'
import { ClashProfile } from './utils'
export interface ProfileItemProps {
item: ProfileQueryResultItem
@@ -43,7 +44,7 @@ export interface ProfileItemProps {
global: undefined | 'info' | 'error' | 'warn'
current: undefined | 'info' | 'error' | 'warn'
}
onClickChains: (item: Profile) => void
onClickChains: (item: ClashProfile) => void
chainsSelected?: boolean
}
@@ -174,7 +175,7 @@ export const ProfileItem = memo(function ProfileItem({
() => ({
Select: () => handleSelect(),
'Edit Info': () => setOpen(true),
'Proxy Chains': () => onClickChains(item),
'Proxy Chains': () => onClickChains(item as ClashProfile),
'Open File': () => item?.view?.(),
Update: () => handleUpdate(),
'Update(Proxy)': () => handleUpdate(true),
@@ -308,7 +309,7 @@ export const ProfileItem = memo(function ProfileItem({
startIcon={<Terminal />}
onClick={(e) => {
cleanDeepClickEvent(e)
onClickChains(item)
onClickChains(item as ClashProfile)
}}
>
{t('Proxy Chains')}

View File

@@ -7,10 +7,11 @@ import clashMetaSchema from 'meta-json-schema/schemas/meta-json-schema.json'
import { type editor } from 'monaco-editor'
import { configureMonacoYaml } from 'monaco-yaml'
import { nanoid } from 'nanoid'
import { useCallback, useMemo } from 'react'
import { useCallback, useMemo, useRef } from 'react'
// schema
import { themeMode } from '@/store'
import MonacoEditor, { type Monaco } from '@monaco-editor/react'
import { openThat } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
export interface ProfileMonacoViewProps {
@@ -57,6 +58,47 @@ export const beforeEditorMount = (monaco: Monaco) => {
},
],
})
// Register link provider for all supported languages
const registerLinkProvider = (language: string) => {
monaco.languages.registerLinkProvider(language, {
provideLinks: (model, token) => {
const links = []
// More robust URL regex pattern
const urlRegex = /\b(?:https?:\/\/|www\.)[^\s<>"']*[^<>\s"',.!?]/gi
for (let i = 1; i <= model.getLineCount(); i++) {
const line = model.getLineContent(i)
let match
while ((match = urlRegex.exec(line)) !== null) {
const url = match[0].startsWith('http')
? match[0]
: `https://${match[0]}`
links.push({
range: new monaco.Range(
i,
match.index + 1,
i,
match.index + match[0].length + 1,
),
url,
})
}
}
return {
links,
dispose: () => {},
}
},
})
}
// Register link provider for all languages we support
registerLinkProvider('javascript')
registerLinkProvider('lua')
registerLinkProvider('yaml')
}
initd = true
}
@@ -77,6 +119,8 @@ export default function ProfileMonacoViewer({
[schemaType, language],
)
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const onChange = useCallback(
(value: string | undefined) => {
if (value && others.onChange) {
@@ -86,6 +130,53 @@ export default function ProfileMonacoViewer({
[others],
)
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
editorRef.current = editor
// Enable URL detection and handling
editor.onMouseDown((e) => {
const position = e.target.position
if (!position) return
// Get the model
const model = editor.getModel()
if (!model) return
// Get the word at the clicked position
const wordAtPosition = model.getWordAtPosition(position)
if (!wordAtPosition) return
// More comprehensive URL regex pattern
const urlRegex = /\b(?:https?:\/\/|www\.)[^\s<>"']*[^<>\s"',.!?]/gi
// Check if the clicked word is part of a URL
const lineContent = model.getLineContent(position.lineNumber)
let match
while ((match = urlRegex.exec(lineContent)) !== null) {
const urlStart = match.index + 1
const urlEnd = urlStart + match[0].length
// Check if the click position is within the URL
if (position.column >= urlStart && position.column <= urlEnd) {
// Only handle Ctrl+Click or Cmd+Click
if (e.event.ctrlKey || e.event.metaKey) {
// Add protocol if missing (for www. URLs)
const url = match[0].startsWith('http')
? match[0]
: `https://${match[0]}`
openThat(url)
e.event.preventDefault()
break
}
}
}
})
},
[],
)
return (
<MonacoEditor
className={cn(className)}
@@ -94,6 +185,7 @@ export default function ProfileMonacoViewer({
path={path}
theme={mode === 'light' ? 'vs' : 'vs-dark'}
beforeMount={beforeEditorMount}
onMount={handleEditorDidMount}
onChange={onChange}
onValidate={onValidate}
options={{
@@ -105,9 +197,7 @@ export default function ProfileMonacoViewer({
automaticLayout: true,
fontLigatures: true,
smoothScrolling: true,
fontFamily: `'Cascadia Code NF', 'Cascadia Code', Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
OS === 'windows' ? ', twemoji mozilla' : ''
}`,
fontFamily: `'Cascadia Code NF', 'Cascadia Code', Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${OS === 'windows' ? ', twemoji mozilla' : ''}`,
quickSuggestions: {
strings: true,
comments: true,

View File

@@ -13,7 +13,12 @@ import {
} from '@nyanpasu/interface'
import { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'
import LanguageChip from './modules/language-chip'
import { getLanguage, ProfileType, ProfileTypes } from './utils'
import {
ChainProfileBuilder,
getLanguage,
ProfileType,
ProfileTypes,
} from './utils'
const ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))
@@ -47,7 +52,10 @@ const optionTypeMapping = [
const convertTypeMapping = (data: Profile) => {
optionTypeMapping.forEach((option) => {
if (option.id === data.type) {
data.type = option.value
data = {
...data,
...option,
}
}
})
@@ -82,7 +90,6 @@ export const ScriptDialog = ({
} else {
form.reset({
type: 'merge',
chain: [],
name: t('New Script'),
desc: '',
})
@@ -93,10 +100,12 @@ export const ScriptDialog = ({
const editor = useReactive<{
value: string
displayLanguage: string
language: string
rawType: ProfileType
}>({
value: ProfileTemplate.merge,
displayLanguage: 'YAML',
language: 'yaml',
rawType: 'merge',
})
@@ -127,13 +136,13 @@ export const ScriptDialog = ({
await contentFn.upsert.mutateAsync(editorValue)
await patch.mutateAsync({
uid: data.uid,
profile: data,
profile: data as ChainProfileBuilder,
})
} else {
await create.mutateAsync({
type: 'manual',
data: {
item: data,
item: data as ChainProfileBuilder,
fileData: editorValue,
},
})
@@ -148,10 +157,12 @@ export const ScriptDialog = ({
const result = await contentFn.query.refetch()
editor.value = result.data ?? ''
editor.language = getLanguage(profile!.type)!
editor.displayLanguage = getLanguage(profile!)
editor.language = editor.displayLanguage.toLowerCase()
} else {
editor.value = ProfileTemplate.merge
editor.language = 'yaml'
editor.displayLanguage = 'YAML'
editor.language = editor.displayLanguage.toLowerCase()
}
setOpenMonaco(open)
@@ -162,15 +173,16 @@ export const ScriptDialog = ({
editor.rawType = convertTypeMapping(data).type
const lang = getLanguage(editor.rawType)
const lang = getLanguage(data)
if (!lang) {
return
}
editor.language = lang
editor.displayLanguage = lang
editor.language = editor.displayLanguage.toLowerCase()
switch (lang) {
switch (editor.language) {
case 'yaml': {
editor.value = ProfileTemplate.merge
break
@@ -191,11 +203,13 @@ export const ScriptDialog = ({
return (
<BaseDialog
title={
<div className="flex gap-2">
<div className="flex gap-2" data-tauri-drag-region>
<span>{isEdit ? t('Edit Script') : t('New Script')}</span>
<LanguageChip
type={isEdit ? (profile?.type ?? editor.rawType) : editor.rawType}
lang={
isEdit && profile ? getLanguage(profile) : editor.displayLanguage
}
/>
</div>
}
@@ -260,9 +274,7 @@ export const ScriptDialog = ({
onValidate={(marks) => {
editorMarks.current = marks
}}
schemaType={
editor.rawType === ProfileTypes.Merge ? 'merge' : undefined
}
schemaType={editor.rawType === 'merge' ? 'merge' : undefined}
/>
)}
</Suspense>

View File

@@ -1,21 +1,23 @@
import { isEqual } from 'lodash-es'
import type {
LocalProfile,
MergeProfile,
Profile,
RemoteProfile,
ScriptProfile,
} from '@nyanpasu/interface'
import type { Profile, ProfileBuilder } from '@nyanpasu/interface'
/**
* Represents a Clash configuration profile, which can be either locally stored or fetched from a remote source.
*/
export type ClashProfile = LocalProfile | RemoteProfile
export type ClashProfile = Extract<Profile, { type: 'remote' | 'local' }>
export type ClashProfileBuilder = Extract<
ProfileBuilder,
{ type: 'remote' | 'local' }
>
/**
* Represents a Clash configuration profile that is a chain of multiple profiles.
*/
export type ChainProfile = MergeProfile | ScriptProfile
export type ChainProfile = Extract<Profile, { type: 'merge' | 'script' }>
export type ChainProfileBuilder = Extract<
ProfileBuilder,
{ type: 'merge' | 'script' }
>
/**
* Filters an array of profiles into two categories: clash and chain profiles.
@@ -43,9 +45,7 @@ export function filterProfiles<T extends Profile>(items?: T[]) {
* @returns {Array<{ type: string | { script: 'javascript' | 'lua' } }>} A filtered array containing only merge items or items with scripts
*/
const chain = items?.filter(
(item) =>
item.type === 'merge' ||
(typeof item.type === 'object' && item.type.script),
(item) => item.type === 'merge' || item.type === 'script',
)
return {
@@ -57,25 +57,24 @@ export function filterProfiles<T extends Profile>(items?: T[]) {
export type ProfileType = Profile['type']
export const ProfileTypes = {
JavaScript: { script: 'javascript' },
LuaScript: { script: 'lua' },
Merge: 'merge',
JavaScript: { type: 'script', script_type: 'javascript' },
LuaScript: { type: 'script', script_type: 'lua' },
Merge: { type: 'merge' },
} as const
export const getLanguage = (type: ProfileType, snake?: boolean) => {
switch (true) {
case isEqual(type, ProfileTypes.JavaScript):
case isEqual(type, ProfileTypes.JavaScript.script): {
return snake ? 'JavaScript' : 'javascript'
}
case isEqual(type, ProfileTypes.LuaScript):
case isEqual(type, ProfileTypes.LuaScript.script): {
return snake ? 'Lua' : 'lua'
}
case isEqual(type, ProfileTypes.Merge): {
return snake ? 'YAML' : 'yaml'
}
export const getLanguage = (profile: Profile) => {
switch (profile.type) {
case 'script':
switch (profile.script_type) {
case 'javascript':
return 'JavaScript'
case 'lua':
return 'Lua'
}
break
case 'merge':
case 'local':
case 'remote':
return 'YAML'
}
}

View File

@@ -0,0 +1,117 @@
import { useTranslation } from 'react-i18next'
import { useSetting } from '@nyanpasu/interface'
import { SwitchItem } from '@nyanpasu/ui'
// 定义各语言的翻译文本
const translations = {
'zh-CN': {
proxy: '当代理切换时打断连接',
profile: '当配置文件切换时打断连接',
mode: '当模式切换时打断连接',
},
'zh-TW': {
proxy: '當代理切換時打斷連線',
profile: '當設定檔切換時打斷連線',
mode: '當模式切換時打斷連線',
},
ru: {
proxy: 'Прерывать соединения при смене прокси',
profile: 'Прерывать соединения при смене профиля',
mode: 'Прерывать соединения при смене режима',
},
en: {
proxy: 'Interrupt connections when proxy changes',
profile: 'Interrupt connections when profile changes',
mode: 'Interrupt connections when mode changes',
},
// 默认使用英文
default: {
proxy: 'Interrupt connections when proxy changes',
profile: 'Interrupt connections when profile changes',
mode: 'Interrupt connections when mode changes',
},
}
const BreakWhenProxyChangeSetting = () => {
const { i18n } = useTranslation()
const currentLang = i18n.language
// 获取当前语言的翻译,如果找不到则使用默认英文
const currentTranslations =
translations[currentLang as keyof typeof translations] ||
translations.default
const { value, upsert } = useSetting('break_when_proxy_change' as any)
return (
<SwitchItem
label={currentTranslations.proxy}
checked={value !== 'none'}
onChange={() => {
if (value === 'none') {
upsert('all' as any)
} else {
upsert('none' as any)
}
}}
/>
)
}
const BreakWhenProfileChangeSetting = () => {
const { i18n } = useTranslation()
const currentLang = i18n.language
// 获取当前语言的翻译,如果找不到则使用默认英文
const currentTranslations =
translations[currentLang as keyof typeof translations] ||
translations.default
const { value, upsert } = useSetting('break_when_profile_change' as any)
return (
<SwitchItem
label={currentTranslations.profile}
checked={value === true}
onChange={() => {
if (value === true) {
upsert(false as any)
} else {
upsert(true as any)
}
}}
/>
)
}
const BreakWhenModeChangeSetting = () => {
const { i18n } = useTranslation()
const currentLang = i18n.language
// 获取当前语言的翻译,如果找不到则使用默认英文
const currentTranslations =
translations[currentLang as keyof typeof translations] ||
translations.default
const { value, upsert } = useSetting('break_when_mode_change' as any)
return (
<SwitchItem
label={currentTranslations.mode}
checked={value === true}
onChange={() => {
if (value === true) {
upsert(false as any)
} else {
upsert(true as any)
}
}}
/>
)
}
export {
BreakWhenProxyChangeSetting,
BreakWhenProfileChangeSetting,
BreakWhenModeChangeSetting,
}

View File

@@ -7,20 +7,11 @@ import {
type NetworkStatisticWidgetConfig,
} from '@nyanpasu/interface'
import { BaseCard, MenuItem, SwitchItem, TextItem } from '@nyanpasu/ui'
const AutoCloseConnection = () => {
const { t } = useTranslation()
const { value, upsert } = useSetting('auto_close_connection')
return (
<SwitchItem
label={t('Auto Close Connections')}
checked={Boolean(value)}
onChange={() => upsert(!value)}
/>
)
}
import {
BreakWhenModeChangeSetting,
BreakWhenProfileChangeSetting,
BreakWhenProxyChangeSetting,
} from './setting-nyanpasu-auto-reload'
const EnableBuiltinEnhanced = () => {
const { t } = useTranslation()
@@ -163,7 +154,11 @@ export const SettingNyanpasuMisc = () => {
<NetworkWidgetVariant />
<AutoCloseConnection />
<BreakWhenProxyChangeSetting />
<BreakWhenProfileChangeSetting />
<BreakWhenModeChangeSetting />
<EnableBuiltinEnhanced />

View File

@@ -37,7 +37,7 @@ import { zodSearchValidator } from '@tanstack/router-zod-adapter'
const profileSearchParams = z.object({
subscribeName: z.string().optional(),
subscribeUrl: z.string().url().optional(),
subscribeUrl: z.url().optional(),
subscribeDesc: z.string().optional(),
})

View File

@@ -5,6 +5,7 @@ import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.
import 'monaco-editor/esm/vs/basic-languages/lua/lua.contribution.js'
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js'
import 'monaco-editor/esm/vs/editor/editor.all.js'
import 'monaco-editor/esm/vs/editor/contrib/links/browser/links.js'
// language services
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
import 'monaco-editor/esm/vs/language/typescript/monaco.contribution.js'

View File

@@ -23,7 +23,7 @@
"@vitejs/plugin-react": "5.0.2",
"ahooks": "3.9.5",
"d3": "7.9.0",
"framer-motion": "12.23.12",
"framer-motion": "12.23.13",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-error-boundary": "6.0.0",

View File

@@ -43,5 +43,23 @@
"info": {
"grant_core_permission": "Clash core needs admin permission to make TUN mode work properly, grant it?\nPlease note that this operation requires password input."
}
}
},
"setting": {
"connection": {
"interrupt": {
"proxy": {
"label": "Interrupt connections when proxy changes"
},
"profile": {
"label": "Interrupt connections when profile changes"
},
"mode": {
"label": "Interrupt connections when mode changes"
}
}
}
},
"break_when_proxy_change": "Interrupt connections when proxy changes",
"break_when_profile_change": "Interrupt connections when profile changes",
"break_when_mode_change": "Interrupt connections when mode changes"
}

View File

@@ -35,13 +35,31 @@
},
"dialog": {
"panic": "Пожалуйста, сообщите об этой проблеме в трекере проблем Github.",
"migrate": "Обнаружен файл конфигурации старой версии\nМигрировать на новую версию или нет?\n ВНИМАНИЕ: Это перезапишет вашу текущую конфигурацию, если она существует",
"custom_app_dir_migrate": "Вы установите пользовательскую папку приложения в %{path}\nПереместить ли текущую папку приложения в новую?",
"migrate": "Обнаружен файл конфигурации старой версии\\nМигрировать на новую версию или нет?\\n ВНИМАНИЕ: Это перезапишет вашу текущую конфигурацию, если она существует",
"custom_app_dir_migrate": "Вы установите пользовательскую папку приложения в %{path}\\ереместить ли текущую папку приложения в новую?",
"warning": {
"enable_tun_with_no_permission": "Режим TUN требует прав администратора или режима службы, ни один из которых не включен, режим TUN не будет работать должным образом."
},
"info": {
"grant_core_permission": "Ядру Clash необходимы права администратора для корректной работы режима TUN, предоставить их?\n\nОбратите внимание: Эта операция требует ввода пароля."
"grant_core_permission": "Ядру Clash необходимы права администратора для корректной работы режима TUN, предоставить их?\\n\\nОбратите внимание: Эта операция требует ввода пароля."
}
}
},
"setting": {
"connection": {
"interrupt": {
"proxy": {
"label": "Прерывать соединения при смене прокси"
},
"profile": {
"label": "Прерывать соединения при смене профиля"
},
"mode": {
"label": "Прерывать соединения при смене режима"
}
}
}
},
"break_when_proxy_change": "Прерывать соединения при смене прокси",
"break_when_profile_change": "Прерывать соединения при смене профиля",
"break_when_mode_change": "Прерывать соединения при смене режима"
}

View File

@@ -43,5 +43,23 @@
"info": {
"grant_core_permission": "Clash 内核需要管理员权限才能使得 TUN 模式正常工作,是否授予?\n\n请注意此操作需要输入密码。"
}
}
},
"setting": {
"connection": {
"interrupt": {
"proxy": {
"label": "当代理切换时打断连接"
},
"profile": {
"label": "当配置文件切换时打断连接"
},
"mode": {
"label": "当模式切换时打断连接"
}
}
}
},
"break_when_proxy_change": "当代理切换时打断连接",
"break_when_profile_change": "当配置文件切换时打断连接",
"break_when_mode_change": "当模式切换时打断连接"
}

View File

@@ -43,5 +43,23 @@
"info": {
"grant_core_permission": "Clash 核心需要系統管理員權限才能使 TUN 模式正常工作,是否授予?\n請注意此操作需要輸入密碼。"
}
}
},
"setting": {
"connection": {
"interrupt": {
"proxy": {
"label": "當代理切換時打斷連線"
},
"profile": {
"label": "當設定檔切換時打斷連線"
},
"mode": {
"label": "當模式切換時打斷連線"
}
}
}
},
"break_when_proxy_change": "當代理切換時打斷連線",
"break_when_profile_change": "當設定檔切換時打斷連線",
"break_when_mode_change": "當模式切換時打斷連線"
}

View File

@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.13",
"mihomo_alpha": "alpha-f02766a",
"mihomo_alpha": "alpha-8cdfd87",
"clash_rs": "v0.9.0",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.0-alpha+sha.50f295d"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2025-09-14T22:20:38.183Z"
"updated_at": "2025-09-15T22:21:00.667Z"
}

View File

@@ -253,7 +253,7 @@ importers:
version: 4.1.13
'@tanstack/router-zod-adapter':
specifier: 1.81.5
version: 1.81.5(@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(zod@4.1.8)
version: 1.81.5(@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(zod@4.1.8)
'@tauri-apps/api':
specifier: 2.8.0
version: 2.8.0
@@ -276,8 +276,8 @@ importers:
specifier: 1.11.18
version: 1.11.18
framer-motion:
specifier: 12.23.12
version: 12.23.12(@emotion/is-prop-valid@1.3.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
specifier: 12.23.13
version: 12.23.13(@emotion/is-prop-valid@1.3.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
i18next:
specifier: 25.5.2
version: 25.5.2(typescript@5.9.2)
@@ -355,14 +355,14 @@ importers:
specifier: 5.87.4
version: 5.87.4(react@19.1.1)
'@tanstack/react-router':
specifier: 1.131.41
version: 1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
specifier: 1.131.44
version: 1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/react-router-devtools':
specifier: 1.131.42
version: 1.131.42(@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.41)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5)(tiny-invariant@1.3.3)
specifier: 1.131.44
version: 1.131.44(@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.44)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5)(tiny-invariant@1.3.3)
'@tanstack/router-plugin':
specifier: 1.131.43
version: 1.131.43(@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.1)(jiti@2.5.1)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))
specifier: 1.131.44
version: 1.131.44(@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.1)(jiti@2.5.1)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))
'@tauri-apps/plugin-clipboard-manager':
specifier: 2.3.0
version: 2.3.0
@@ -499,8 +499,8 @@ importers:
specifier: 7.9.0
version: 7.9.0
framer-motion:
specifier: 12.23.12
version: 12.23.12(@emotion/is-prop-valid@1.3.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
specifier: 12.23.13
version: 12.23.13(@emotion/is-prop-valid@1.3.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react:
specifier: 19.1.1
version: 19.1.1
@@ -563,8 +563,8 @@ importers:
specifier: 7.7.1
version: 7.7.1
figlet:
specifier: 1.9.1
version: 1.9.1
specifier: 1.9.2
version: 1.9.2
filesize:
specifier: 11.0.2
version: 11.0.2
@@ -597,8 +597,8 @@ importers:
specifier: 3.4.2
version: 3.4.2
fs-extra:
specifier: 11.3.1
version: 11.3.1
specifier: 11.3.2
version: 11.3.2
octokit:
specifier: 5.0.3
version: 5.0.3
@@ -3028,16 +3028,16 @@ packages:
peerDependencies:
react: ^18 || ^19
'@tanstack/react-router-devtools@1.131.42':
resolution: {integrity: sha512-7pymFB1CCimRHot2Zp0ZekQjd1iN812V88n9NLPSeiv9sVRtRVIaLphJjDeudx1NNgkfSJPx2lOhz6K38cuZog==}
'@tanstack/react-router-devtools@1.131.44':
resolution: {integrity: sha512-JGICSLe3ZIqayo2Pz9bpCBLrK8NIruYSQoe/JkZimSGltV3HU+uPb1dohw0CpyxVuhx+tDqFBzq4cDPCABs4/w==}
engines: {node: '>=12'}
peerDependencies:
'@tanstack/react-router': ^1.131.41
'@tanstack/react-router': ^1.131.44
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
'@tanstack/react-router@1.131.41':
resolution: {integrity: sha512-QEbTYpAosiD8e4qEZRr9aJipGSb8pQc+pfZwK6NCD2Tcxwu2oF6MVtwv0bIDLRpZP0VJMBpxXlTRISUDNMNqIA==}
'@tanstack/react-router@1.131.44':
resolution: {integrity: sha512-LREJfrl8lSedXHCRAAt0HvnHFP9ikAQWnVhYRM++B26w4ZYQBbLvgCT1BCDZVY7MR6rslcd4OfgpZMOyVhNzFg==}
engines: {node: '>=12'}
peerDependencies:
react: '>=18.0.0 || >=19.0.0'
@@ -3062,15 +3062,15 @@ 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
'@tanstack/router-core@1.131.41':
resolution: {integrity: sha512-VoLly00DWM0abKuVPRm8wiwGtRBHOKs6K896fy48Q/KYoDVLs8kRCRjFGS7rGnYC2FIkmmvHqYRqNg7jgCx2yg==}
'@tanstack/router-core@1.131.44':
resolution: {integrity: sha512-Npi9xB3GSYZhRW8+gPhP6bEbyx0vNc8ZNwsi0JapdiFpIiszgRJ57pesy/rklruv46gYQjLVA5KDOsuaCT/urA==}
engines: {node: '>=12'}
'@tanstack/router-devtools-core@1.131.42':
resolution: {integrity: sha512-o8jKTiwXcUSjmkozcMjIw1yhjVYeXcuQO7DtfgjKW3B85iveH6VzYK+bGEVU7wmLNMuUSe2eI/7RBzJ6a5+MCA==}
'@tanstack/router-devtools-core@1.131.44':
resolution: {integrity: sha512-ZpQfRERLAjZ2NBdFOWjlrbMzQ+23aGs+9324KVdLzZkcd1lc0ztpLb5HAGtqLXfncvO60TfiRz106ygjKsaJow==}
engines: {node: '>=12'}
peerDependencies:
'@tanstack/router-core': ^1.131.41
'@tanstack/router-core': ^1.131.44
csstype: ^3.0.10
solid-js: '>=1.9.5'
tiny-invariant: ^1.3.3
@@ -3078,16 +3078,16 @@ packages:
csstype:
optional: true
'@tanstack/router-generator@1.131.41':
resolution: {integrity: sha512-HsDkBU1u/KvHrzn76v/9oeyMFuxvVlE3dfIu4fldZbPy/i903DWBwODIDGe6fVUsYtzPPrRvNtbjV18HVz5GCA==}
'@tanstack/router-generator@1.131.44':
resolution: {integrity: sha512-CnrlRkGatdQXdvTteflOTMANupb1z59CO3DSV+UzBkTG+g+vfWgJeKQ0EkfwZ2QuS6Su2v5r5EMHs/AookeZZw==}
engines: {node: '>=12'}
'@tanstack/router-plugin@1.131.43':
resolution: {integrity: sha512-vBPBw5LBl+ogGZnFVyLmH65rYnr88cKRT1WtDZ+QYNsgto/SQbD+JxJgbm8YJdpteo3KZL6zHyZz30nmwbhC4A==}
'@tanstack/router-plugin@1.131.44':
resolution: {integrity: sha512-CvheUPlB8vxXf23RSDz6q97l1EI5H3f+1qJ/LEBvy7bhls8vYouJ3xyTeu4faz8bEEieLUoVQrCcr+xFY0lkuw==}
engines: {node: '>=12'}
peerDependencies:
'@rsbuild/core': '>=1.0.2'
'@tanstack/react-router': ^1.131.41
'@tanstack/react-router': ^1.131.44
vite: '>=5.0.0 || >=6.0.0'
vite-plugin-solid: ^2.11.2
webpack: '>=5.92.0'
@@ -5192,8 +5192,8 @@ packages:
picomatch:
optional: true
figlet@1.9.1:
resolution: {integrity: sha512-DpKC89iXFjq6NCaIQ1O91R+ofpfyvkwuBfoVd8LpP/JZGZQpelMXgiTStGOfksuMYaDoBAwFlvx7mk+wX+3rrA==}
figlet@1.9.2:
resolution: {integrity: sha512-rRnXvw0JtuEQtN6jlqa7nZ/N2RY49VsGTKTSj8rk5D7z9gY/0F7Gkt57Er7r/xZZ7UGNHsHUwhIA09URknhyVA==}
engines: {node: '>= 17.0.0'}
hasBin: true
@@ -5255,8 +5255,8 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
framer-motion@12.23.13:
resolution: {integrity: sha512-OMF57Xh0fuTXfJQPtCieYGeU9Fam4SxqPLVz78YI7ATRFrfz8SARtqr1+qv56cX45kPFcIEfkUorVfxlOsjcUg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -5273,8 +5273,8 @@ packages:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
fs-extra@11.3.1:
resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
fs-extra@11.3.2:
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'}
fs-extra@8.1.0:
@@ -11002,7 +11002,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.1
fs-extra: 11.3.2
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.8
@@ -11291,10 +11291,10 @@ snapshots:
'@tanstack/query-core': 5.87.4
react: 19.1.1
'@tanstack/react-router-devtools@1.131.42(@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.41)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5)(tiny-invariant@1.3.3)':
'@tanstack/react-router-devtools@1.131.44(@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.44)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5)(tiny-invariant@1.3.3)':
dependencies:
'@tanstack/react-router': 1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/router-devtools-core': 1.131.42(@tanstack/router-core@1.131.41)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)
'@tanstack/react-router': 1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/router-devtools-core': 1.131.44(@tanstack/router-core@1.131.44)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
transitivePeerDependencies:
@@ -11303,11 +11303,11 @@ snapshots:
- solid-js
- tiny-invariant
'@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
'@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@tanstack/history': 1.131.2
'@tanstack/react-store': 0.7.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/router-core': 1.131.41
'@tanstack/router-core': 1.131.44
isbot: 5.1.28
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
@@ -11333,7 +11333,7 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
'@tanstack/router-core@1.131.41':
'@tanstack/router-core@1.131.44':
dependencies:
'@tanstack/history': 1.131.2
'@tanstack/store': 0.7.0
@@ -11343,9 +11343,9 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
'@tanstack/router-devtools-core@1.131.42(@tanstack/router-core@1.131.41)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)':
'@tanstack/router-devtools-core@1.131.44(@tanstack/router-core@1.131.44)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)':
dependencies:
'@tanstack/router-core': 1.131.41
'@tanstack/router-core': 1.131.44
clsx: 2.1.1
goober: 2.1.16(csstype@3.1.3)
solid-js: 1.9.5
@@ -11353,9 +11353,9 @@ snapshots:
optionalDependencies:
csstype: 3.1.3
'@tanstack/router-generator@1.131.41':
'@tanstack/router-generator@1.131.44':
dependencies:
'@tanstack/router-core': 1.131.41
'@tanstack/router-core': 1.131.44
'@tanstack/router-utils': 1.131.2
'@tanstack/virtual-file-routes': 1.131.2
prettier: 3.6.2
@@ -11366,7 +11366,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-plugin@1.131.43(@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.1)(jiti@2.5.1)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))':
'@tanstack/router-plugin@1.131.44(@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.1)(jiti@2.5.1)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.3
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3)
@@ -11374,8 +11374,8 @@ snapshots:
'@babel/template': 7.27.2
'@babel/traverse': 7.28.3
'@babel/types': 7.28.2
'@tanstack/router-core': 1.131.41
'@tanstack/router-generator': 1.131.41
'@tanstack/router-core': 1.131.44
'@tanstack/router-generator': 1.131.44
'@tanstack/router-utils': 1.131.2
'@tanstack/virtual-file-routes': 1.131.2
babel-dead-code-elimination: 1.0.10
@@ -11383,7 +11383,7 @@ snapshots:
unplugin: 2.3.9
zod: 3.25.76
optionalDependencies:
'@tanstack/react-router': 1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/react-router': 1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
vite: 7.1.5(@types/node@24.3.1)(jiti@2.5.1)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@@ -11399,9 +11399,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(zod@4.1.8)':
'@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(zod@4.1.8)':
dependencies:
'@tanstack/react-router': 1.131.41(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/react-router': 1.131.44(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
zod: 4.1.8
'@tanstack/store@0.7.0': {}
@@ -13871,7 +13871,7 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
figlet@1.9.1:
figlet@1.9.2:
dependencies:
commander: 14.0.0
@@ -13938,7 +13938,7 @@ snapshots:
fraction.js@4.3.7: {}
framer-motion@12.23.12(@emotion/is-prop-valid@1.3.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
framer-motion@12.23.13(@emotion/is-prop-valid@1.3.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
@@ -13954,7 +13954,7 @@ snapshots:
jsonfile: 6.1.0
universalify: 2.0.1
fs-extra@11.3.1:
fs-extra@11.3.2:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0

View File

@@ -6,7 +6,7 @@
"@actions/github": "6.0.1",
"@types/figlet": "1.7.0",
"@types/semver": "7.7.1",
"figlet": "1.9.1",
"figlet": "1.9.2",
"filesize": "11.0.2",
"p-retry": "7.0.0",
"semver": "7.7.2",
@@ -19,7 +19,7 @@
"adm-zip": "0.5.16",
"colorize-template": "1.0.0",
"consola": "3.4.2",
"fs-extra": "11.3.1",
"fs-extra": "11.3.2",
"octokit": "5.0.3",
"picocolors": "1.1.1",
"tar": "7.4.3",

View File

@@ -10,7 +10,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.24.4
go-version: 1.25.1
id: go
- name: Check out repository

View File

@@ -1,20 +1,20 @@
module github.com/m13253/dns-over-https/v2
go 1.24
go 1.24.0
require (
github.com/BurntSushi/toml v1.5.0
github.com/gorilla/handlers v1.5.2
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc
github.com/miekg/dns v1.1.66
golang.org/x/net v0.41.0
github.com/miekg/dns v1.1.68
golang.org/x/net v0.44.0
)
require (
github.com/felixge/httpsnoop v1.0.4 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
)

View File

@@ -8,21 +8,23 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc h1:RhT2pjLo3EVRmldbEcBdeRA7CGPWsNEJC+Y/N1aXQbg=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -1,5 +1,5 @@
#
# Copyright (C) 2020 Tobias Maedel <openwrt@tbspace.de>
# Copyright (C) 2020 Sarah Maedel <openwrt@tbspace.de>
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
@@ -7,12 +7,12 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=2.12
PKG_VERSION:=2.13
PKG_RELEASE:=1
PKG_HASH:=b4c047493cac1152203e1ba121ae57267e4899b7bf56eb365e22a933342d31c9
PKG_HASH:=afb5c408392fcec840bd30de9b02a236b0108142024f9853b542b596b0d894e3
PKG_MAINTAINER:=Tobias Maedel <openwrt@tbspace.de>
PKG_MAINTAINER:=Sarah Maedel <openwrt@tbspace.de>
include $(INCLUDE_DIR)/kernel.mk
include $(INCLUDE_DIR)/trusted-firmware-a.mk
@@ -38,6 +38,11 @@ define Trusted-Firmware-A/rk3568
PLAT:=rk3568
endef
define Trusted-Firmware-A/rk3576
BUILD_SUBTARGET:=armv8
PLAT:=rk3576
endef
define Trusted-Firmware-A/rk3588
BUILD_SUBTARGET:=armv8
PLAT:=rk3588
@@ -47,11 +52,12 @@ TFA_TARGETS:= \
rk3328 \
rk3399 \
rk3568 \
rk3576 \
rk3588
ifeq ($(BUILD_VARIANT),rk3399)
M0_GCC_NAME:=gcc-arm
M0_GCC_RELEASE:=11.2-2022.02
M0_GCC_NAME:=arm-gnu-toolchain
M0_GCC_RELEASE:=12.3.rel1
M0_GCC_VERSION:=$(HOST_ARCH)-arm-none-eabi
M0_GCC_SOURCE:=$(M0_GCC_NAME)-$(M0_GCC_RELEASE)-$(M0_GCC_VERSION).tar.xz
@@ -59,9 +65,9 @@ ifeq ($(BUILD_VARIANT),rk3399)
FILE:=$(M0_GCC_SOURCE)
URL:=https://developer.arm.com/-/media/Files/downloads/gnu/$(M0_GCC_RELEASE)/binrel
ifeq ($(HOST_ARCH),aarch64)
HASH:=ef1d82e5894e3908cb7ed49c5485b5b95deefa32872f79c2b5f6f5447cabf55f
HASH:=14c0487d5753f6071d24e568881f7c7e67f80dd83165dec5164b3731394af431
else
HASH:=8c5acd5ae567c0100245b0556941c237369f210bceb196edfe5a2e7532c60326
HASH:=12a2815644318ebcceaf84beabb665d0924b6e79e21048452c5331a56332b309
endif
endef

View File

@@ -58,7 +58,7 @@ jobs:
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.25 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }}
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.25' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
cd $(go env GOROOT)

View File

@@ -1,9 +1,8 @@
package net
import (
"context"
"io"
"net"
"runtime"
"github.com/metacubex/mihomo/common/net/deadline"
@@ -56,9 +55,37 @@ type CountFunc = network.CountFunc
var Pipe = deadline.Pipe
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
defer runtime.KeepAlive(leftConn)
defer runtime.KeepAlive(rightConn)
_ = bufio.CopyConn(context.TODO(), leftConn, rightConn)
func closeWrite(writer io.Closer) error {
if c, ok := common.Cast[network.WriteCloser](writer); ok {
return c.CloseWrite()
}
return writer.Close()
}
// Relay copies between left and right bidirectionally.
// like [bufio.CopyConn] but remove unneeded [context.Context] handle and the cost of [task.Group]
func Relay(leftConn, rightConn net.Conn) {
defer func() {
_ = leftConn.Close()
_ = rightConn.Close()
}()
ch := make(chan struct{})
go func() {
_, err := bufio.Copy(leftConn, rightConn)
if err == nil {
_ = closeWrite(leftConn)
} else {
_ = leftConn.Close()
}
close(ch)
}()
_, err := bufio.Copy(rightConn, leftConn)
if err == nil {
_ = closeWrite(rightConn)
} else {
_ = rightConn.Close()
}
<-ch
}

View File

@@ -2,8 +2,6 @@ package queue
import (
"sync"
"github.com/samber/lo"
)
// Queue is a simple concurrent safe queue
@@ -24,33 +22,32 @@ func (q *Queue[T]) Put(items ...T) {
}
// Pop returns the head of items.
func (q *Queue[T]) Pop() T {
func (q *Queue[T]) Pop() (head T) {
if len(q.items) == 0 {
return lo.Empty[T]()
return
}
q.lock.Lock()
head := q.items[0]
head = q.items[0]
q.items = q.items[1:]
q.lock.Unlock()
return head
}
// Last returns the last of item.
func (q *Queue[T]) Last() T {
func (q *Queue[T]) Last() (last T) {
if len(q.items) == 0 {
return lo.Empty[T]()
return
}
q.lock.RLock()
last := q.items[len(q.items)-1]
last = q.items[len(q.items)-1]
q.lock.RUnlock()
return last
}
// Copy get the copy of queue.
func (q *Queue[T]) Copy() []T {
items := []T{}
func (q *Queue[T]) Copy() (items []T) {
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()

View File

@@ -0,0 +1,215 @@
package queue
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestQueuePut tests the Put method of Queue
func TestQueuePut(t *testing.T) {
// Initialize a new queue
q := New[int](10)
// Test putting a single item
q.Put(1)
assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after putting one item")
// Test putting multiple items
q.Put(2, 3, 4)
assert.Equal(t, int64(4), q.Len(), "Queue length should be 4 after putting three more items")
// Test putting zero items (should not change queue)
q.Put()
assert.Equal(t, int64(4), q.Len(), "Queue length should remain unchanged when putting zero items")
}
// TestQueuePop tests the Pop method of Queue
func TestQueuePop(t *testing.T) {
// Initialize a new queue with items
q := New[int](10)
q.Put(1, 2, 3)
// Test popping items in FIFO order
item := q.Pop()
assert.Equal(t, 1, item, "First item popped should be 1")
assert.Equal(t, int64(2), q.Len(), "Queue length should be 2 after popping one item")
item = q.Pop()
assert.Equal(t, 2, item, "Second item popped should be 2")
assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after popping two items")
item = q.Pop()
assert.Equal(t, 3, item, "Third item popped should be 3")
assert.Equal(t, int64(0), q.Len(), "Queue length should be 0 after popping all items")
}
// TestQueuePopEmpty tests the Pop method on an empty queue
func TestQueuePopEmpty(t *testing.T) {
// Initialize a new empty queue
q := New[int](0)
// Test popping from an empty queue
item := q.Pop()
assert.Equal(t, 0, item, "Popping from an empty queue should return the zero value")
assert.Equal(t, int64(0), q.Len(), "Queue length should remain 0 after popping from an empty queue")
}
// TestQueueLast tests the Last method of Queue
func TestQueueLast(t *testing.T) {
// Initialize a new queue with items
q := New[int](10)
q.Put(1, 2, 3)
// Test getting the last item
item := q.Last()
assert.Equal(t, 3, item, "Last item should be 3")
assert.Equal(t, int64(3), q.Len(), "Queue length should remain unchanged after calling Last")
// Test Last on an empty queue
emptyQ := New[int](0)
emptyItem := emptyQ.Last()
assert.Equal(t, 0, emptyItem, "Last on an empty queue should return the zero value")
}
// TestQueueCopy tests the Copy method of Queue
func TestQueueCopy(t *testing.T) {
// Initialize a new queue with items
q := New[int](10)
q.Put(1, 2, 3)
// Test copying the queue
copy := q.Copy()
assert.Equal(t, 3, len(copy), "Copy should have the same number of items as the original queue")
assert.Equal(t, 1, copy[0], "First item in copy should be 1")
assert.Equal(t, 2, copy[1], "Second item in copy should be 2")
assert.Equal(t, 3, copy[2], "Third item in copy should be 3")
// Verify that modifying the copy doesn't affect the original queue
copy[0] = 99
assert.Equal(t, 1, q.Pop(), "Original queue should not be affected by modifying the copy")
}
// TestQueueLen tests the Len method of Queue
func TestQueueLen(t *testing.T) {
// Initialize a new empty queue
q := New[int](10)
assert.Equal(t, int64(0), q.Len(), "New queue should have length 0")
// Add items and check length
q.Put(1, 2)
assert.Equal(t, int64(2), q.Len(), "Queue length should be 2 after putting two items")
// Remove an item and check length
q.Pop()
assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after popping one item")
}
// TestQueueNew tests the New constructor
func TestQueueNew(t *testing.T) {
// Test creating a new queue with different hints
q1 := New[int](0)
assert.NotNil(t, q1, "New queue should not be nil")
assert.Equal(t, int64(0), q1.Len(), "New queue should have length 0")
q2 := New[int](10)
assert.NotNil(t, q2, "New queue should not be nil")
assert.Equal(t, int64(0), q2.Len(), "New queue should have length 0")
// Test with a different type
q3 := New[string](5)
assert.NotNil(t, q3, "New queue should not be nil")
assert.Equal(t, int64(0), q3.Len(), "New queue should have length 0")
}
// TestQueueConcurrency tests the concurrency safety of Queue
func TestQueueConcurrency(t *testing.T) {
// Initialize a new queue
q := New[int](100)
// Number of goroutines and operations
goroutines := 10
operations := 100
// Wait group to synchronize goroutines
wg := sync.WaitGroup{}
wg.Add(goroutines * 2) // For both producers and consumers
// Start producer goroutines
for i := 0; i < goroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < operations; j++ {
q.Put(id*operations + j)
// Small sleep to increase chance of race conditions
time.Sleep(time.Microsecond)
}
}(i)
}
// Start consumer goroutines
consumed := make(chan int, goroutines*operations)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operations; j++ {
// Try to pop an item, but don't block if queue is empty
// Use a mutex to avoid race condition between Len() check and Pop()
q.lock.Lock()
if len(q.items) > 0 {
item := q.items[0]
q.items = q.items[1:]
q.lock.Unlock()
consumed <- item
} else {
q.lock.Unlock()
}
// Small sleep to increase chance of race conditions
time.Sleep(time.Microsecond)
}
}()
}
// Wait for all goroutines to finish
wg.Wait()
// Close the consumed channel
close(consumed)
// Count the number of consumed items
consumedCount := 0
for range consumed {
consumedCount++
}
// Check that the queue is in a consistent state
totalItems := goroutines * operations
remaining := int(q.Len())
assert.Equal(t, totalItems, consumedCount+remaining, "Total items should equal consumed items plus remaining items")
}
// TestQueueWithDifferentTypes tests the Queue with different types
func TestQueueWithDifferentTypes(t *testing.T) {
// Test with string type
qString := New[string](5)
qString.Put("hello", "world")
assert.Equal(t, int64(2), qString.Len(), "Queue length should be 2")
assert.Equal(t, "hello", qString.Pop(), "First item should be 'hello'")
assert.Equal(t, "world", qString.Pop(), "Second item should be 'world'")
// Test with struct type
type Person struct {
Name string
Age int
}
qStruct := New[Person](5)
qStruct.Put(Person{Name: "Alice", Age: 30}, Person{Name: "Bob", Age: 25})
assert.Equal(t, int64(2), qStruct.Len(), "Queue length should be 2")
firstPerson := qStruct.Pop()
assert.Equal(t, "Alice", firstPerson.Name, "First person's name should be 'Alice'")
secondPerson := qStruct.Pop()
assert.Equal(t, "Bob", secondPerson.Name, "Second person's name should be 'Bob'")
}

View File

@@ -35,7 +35,7 @@ require (
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142
github.com/metacubex/utls v1.8.1-0.20250916021850-3fcad0728a32
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0

View File

@@ -139,8 +139,8 @@ github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113a
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 h1:yN3mQ4cT9sPUciw/rO0Isc/8QlO86DB6g9SEMRgQ8Cw=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 h1:csEbKOzRAxJXffOeZnnS3/kA/F55JiTbKv5jcYqCXms=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U=
github.com/metacubex/utls v1.8.1-0.20250916021850-3fcad0728a32 h1:endaN8dWxRofYpmJS46mPMQdzNyGEOwvXva42P8RY3I=
github.com/metacubex/utls v1.8.1-0.20250916021850-3fcad0728a32/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=

View File

@@ -58,7 +58,7 @@ define Download/adguardhome-frontend
URL:=https://github.com/AdguardTeam/AdGuardHome/releases/download/v$(PKG_VERSION)/
URL_FILE:=AdGuardHome_frontend.tar.gz
FILE:=$(FRONTEND_FILE)
HASH:=18ead3a9a0c710a05d63a3f967795709120a8f50e8938462860022ada3c950e4
HASH:=18ead3a9a0c710a05d63a3f967795709120a8f50e8938462860022ada3c950e4
endef
define Build/Prepare

View File

@@ -37,55 +37,57 @@ local api = require "luci.passwall.api"
<div id="upload-modal" class="up-modal" style="display:none;">
<div class="up-modal-content">
<h3><%:Restore Backup File%></h3>
<div class="cbi-value" id="_upload_div">
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" accept=".tar.gz" />
<br />
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" onclick="do_upload()" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_upload_win()" value="<%:CLOSE WIN%>" />
</div>
</div>
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" accept=".tar.gz" />
</div>
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" onclick="do_upload()" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_upload_win()" value="<%:CLOSE WIN%>" />
</div>
</div>
</div>
<style>
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
}
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 400px;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 15px;
margin-bottom: 30px;
}
</style>
<script>

View File

@@ -9,9 +9,9 @@ PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git
PKG_SOURCE_DATE:=2025-09-14
PKG_SOURCE_VERSION:=aa3ecaecac29e23df1ee04e48b41bd274abd50ce
PKG_MIRROR_HASH:=2e846c5a5628d4b8af1763b2bd79dbdf6d252ec1a5411209c18fba989351af55
PKG_SOURCE_DATE:=2025-09-17
PKG_SOURCE_VERSION:=9f1c58396d58d04584012dfa1862c535e9acee05
PKG_MIRROR_HASH:=a2aa6d34108ae5dd56ce6aeeb84884a3ca1a42cf4a069607944681097cbb5d69
PKG_LICENSE:=BSD 3-Clause
PKG_LICENSE_FILES:=LICENSE

View File

@@ -3,7 +3,7 @@
#ifndef OUT_LAST_COMMIT_POSITION_H_
#define OUT_LAST_COMMIT_POSITION_H_
#define LAST_COMMIT_POSITION_NUM 2282
#define LAST_COMMIT_POSITION "2282 (aa3ecaecac29)"
#define LAST_COMMIT_POSITION_NUM 2283
#define LAST_COMMIT_POSITION "2283 (9f1c58396d58)"
#endif // OUT_LAST_COMMIT_POSITION_H_

View File

@@ -37,55 +37,57 @@ local api = require "luci.passwall.api"
<div id="upload-modal" class="up-modal" style="display:none;">
<div class="up-modal-content">
<h3><%:Restore Backup File%></h3>
<div class="cbi-value" id="_upload_div">
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" accept=".tar.gz" />
<br />
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" onclick="do_upload()" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_upload_win()" value="<%:CLOSE WIN%>" />
</div>
</div>
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" accept=".tar.gz" />
</div>
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" onclick="do_upload()" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_upload_win()" value="<%:CLOSE WIN%>" />
</div>
</div>
</div>
<style>
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
}
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 400px;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 15px;
margin-bottom: 30px;
}
</style>
<script>

View File

@@ -381,9 +381,9 @@ function import_ssr_url(btn, urlname, sid) {
switch (params.get("type")) {
case "ws":
if (params.get("security") !== "tls") {
setElementValue('cbid.shadowsocksr.' + sid + '.ws_host', params.get("host") ? decodeURIComponent(params.get("host")) : "");
document.getElementsByName('cbid.shadowsocksr.' + sid + '.ws_host')[0].value = params.get("host") ? decodeURIComponent(params.get("host")) : "";
}
setElementValue('cbid.shadowsocksr.' + sid + '.ws_path', params.get("path") ? decodeURIComponent(params.get("path")) : "/");
document.getElementsByName('cbid.shadowsocksr.' + sid + '.ws_path')[0].value = params.get("path") ? decodeURIComponent(params.get("path")) : "/";
break;
case "httpupgrade":
if (params.get("security") !== "tls") {

View File

@@ -21,13 +21,13 @@ define Download/geoip
HASH:=a01e09150b456cb2f3819d29d6e6c34572420aaee3ff9ef23977c4e9596c20ec
endef
GEOSITE_VER:=20250906011216
GEOSITE_VER:=20250916122507
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:=186158b6c2f67ac59e184ed997ebebcef31938be9874eb8a7d5e3854187f4e8d
HASH:=1a7dad0ceaaf1f6d12fef585576789699bd1c6ea014c887c04b94cb9609350e9
endef
GEOSITE_IRAN_VER:=202509150040

View File

@@ -96,12 +96,12 @@ class VKIE(VKBaseIE):
https?://
(?:
(?:
(?:(?:m|new)\.)?vk(?:(?:video)?\.ru|\.com)/video_|
(?:(?:m|new|vksport)\.)?vk(?:(?:video)?\.ru|\.com)/video_|
(?:www\.)?daxab\.com/
)
ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)|
(?:
(?:(?:m|new)\.)?vk(?:(?:video)?\.ru|\.com)/(?:.+?\?.*?z=)?(?:video|clip)|
(?:(?:m|new|vksport)\.)?vk(?:(?:video)?\.ru|\.com)/(?:.+?\?.*?z=)?(?:video|clip)|
(?:www\.)?daxab\.com/embed/
)
(?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>([\da-f]+)|(ln-[\da-zA-Z]+)))?
@@ -359,6 +359,10 @@ class VKIE(VKBaseIE):
'url': 'https://vk.ru/video-220754053_456242564',
'only_matching': True,
},
{
'url': 'https://vksport.vkvideo.ru/video-124096712_456240773',
'only_matching': True,
},
]
def _real_extract(self, url):