mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
Update On Tue Sep 16 20:39:27 CEST 2025
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -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
|
||||
|
2
clash-meta/.github/workflows/test.yml
vendored
2
clash-meta/.github/workflows/test.yml
vendored
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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()
|
||||
|
215
clash-meta/common/queue/queue_test.go
Normal file
215
clash-meta/common/queue/queue_test.go
Normal 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'")
|
||||
}
|
@@ -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
|
||||
|
@@ -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=
|
||||
|
2
clash-nyanpasu/.github/workflows/stale.yml
vendored
2
clash-nyanpasu/.github/workflows/stale.yml
vendored
@@ -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.'
|
||||
|
60
clash-nyanpasu/backend/Cargo.lock
generated
60
clash-nyanpasu/backend/Cargo.lock
generated
@@ -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]]
|
||||
|
@@ -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.
|
||||
|
@@ -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 = [
|
||||
|
@@ -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" => {
|
||||
|
@@ -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),
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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 {}
|
||||
|
@@ -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 {}
|
||||
|
@@ -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 {
|
||||
|
@@ -1,2 +1,4 @@
|
||||
#![allow(unused_imports)]
|
||||
pub use super::{ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileSharedGetter};
|
||||
pub use super::{
|
||||
ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileKindGetter, ProfileMetaGetter,
|
||||
};
|
||||
|
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
286
clash-nyanpasu/backend/tauri/src/config/profile/tests.rs
Normal file
286
clash-nyanpasu/backend/tauri/src/config/profile/tests.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {}'""#;
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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(),
|
||||
]
|
||||
});
|
||||
|
||||
|
@@ -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:#?}");
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -3,7 +3,7 @@ use super::super::{
|
||||
task::{Task, TaskID, TaskManager, TaskSchedule},
|
||||
};
|
||||
use crate::{
|
||||
config::{Config, ProfileSharedGetter},
|
||||
config::{Config, ProfileMetaGetter},
|
||||
feat,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
// 切换系统代理
|
||||
|
@@ -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(())
|
||||
}
|
||||
|
||||
|
@@ -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.",
|
||||
));
|
||||
|
@@ -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'
|
||||
|
@@ -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
|
||||
|
1
clash-nyanpasu/frontend/nyanpasu/.gitignore
vendored
Normal file
1
clash-nyanpasu/frontend/nyanpasu/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.tanstack
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
},
|
||||
})
|
||||
|
@@ -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) {
|
||||
|
@@ -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')}
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -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'
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
@@ -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 />
|
||||
|
||||
|
@@ -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(),
|
||||
})
|
||||
|
||||
|
@@ -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'
|
||||
|
@@ -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",
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -35,13 +35,31 @@
|
||||
},
|
||||
"dialog": {
|
||||
"panic": "Пожалуйста, сообщите об этой проблеме в трекере проблем Github.",
|
||||
"migrate": "Обнаружен файл конфигурации старой версии\nМигрировать на новую версию или нет?\n ВНИМАНИЕ: Это перезапишет вашу текущую конфигурацию, если она существует",
|
||||
"custom_app_dir_migrate": "Вы установите пользовательскую папку приложения в %{path}\nПереместить ли текущую папку приложения в новую?",
|
||||
"migrate": "Обнаружен файл конфигурации старой версии\\nМигрировать на новую версию или нет?\\n ВНИМАНИЕ: Это перезапишет вашу текущую конфигурацию, если она существует",
|
||||
"custom_app_dir_migrate": "Вы установите пользовательскую папку приложения в %{path}\\nПереместить ли текущую папку приложения в новую?",
|
||||
"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": "Прерывать соединения при смене режима"
|
||||
}
|
||||
|
@@ -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": "当模式切换时打断连接"
|
||||
}
|
||||
|
@@ -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": "當模式切換時打斷連線"
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
112
clash-nyanpasu/pnpm-lock.yaml
generated
112
clash-nyanpasu/pnpm-lock.yaml
generated
@@ -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
|
||||
|
@@ -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",
|
||||
|
2
dns-over-https/.github/workflows/go.yml
vendored
2
dns-over-https/.github/workflows/go.yml
vendored
@@ -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
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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=
|
||||
|
@@ -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
|
||||
|
||||
|
2
mihomo/.github/workflows/test.yml
vendored
2
mihomo/.github/workflows/test.yml
vendored
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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()
|
||||
|
215
mihomo/common/queue/queue_test.go
Normal file
215
mihomo/common/queue/queue_test.go
Normal 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'")
|
||||
}
|
@@ -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
|
||||
|
@@ -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=
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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_
|
||||
|
@@ -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>
|
||||
|
@@ -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") {
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
Reference in New Issue
Block a user