Update On Thu Mar 27 19:36:13 CET 2025

This commit is contained in:
github-action[bot]
2025-03-27 19:36:13 +01:00
parent d67f57743a
commit a4eec905b6
117 changed files with 1460 additions and 913 deletions

1
.github/update.log vendored
View File

@@ -954,3 +954,4 @@ Update On Sun Mar 23 19:33:38 CET 2025
Update On Mon Mar 24 19:37:20 CET 2025 Update On Mon Mar 24 19:37:20 CET 2025
Update On Tue Mar 25 19:35:52 CET 2025 Update On Tue Mar 25 19:35:52 CET 2025
Update On Wed Mar 26 19:37:44 CET 2025 Update On Wed Mar 26 19:37:44 CET 2025
Update On Thu Mar 27 19:36:04 CET 2025

View File

@@ -174,6 +174,12 @@ func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) {
return return
} }
// It seems that mihomo does not implement a connection error reporting mechanism, so we report success directly.
err = stream.HandshakeSuccess()
if err != nil {
return
}
h.NewConnection(ctx, stream, M.Metadata{ h.NewConnection(ctx, stream, M.Metadata{
Source: M.SocksaddrFromNet(conn.RemoteAddr()), Source: M.SocksaddrFromNet(conn.RemoteAddr()),
Destination: destination, Destination: destination,

View File

@@ -15,10 +15,10 @@ import (
const CheckMark = -1 const CheckMark = -1
var DefaultPaddingScheme = []byte(`stop=8 var DefaultPaddingScheme = []byte(`stop=8
0=34-120 0=30-30
1=100-400 1=100-400
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=500-1000 3=9-9,500-1000
4=500-1000 4=500-1000
5=500-1000 5=500-1000
6=500-1000 6=500-1000

View File

@@ -83,11 +83,7 @@ func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) {
} }
stream.dieHook = func() { stream.dieHook = func() {
if session.IsClosed() { if !session.IsClosed() {
if session.dieHook != nil {
session.dieHook()
}
} else {
select { select {
case <-c.die.Done(): case <-c.die.Done():
// Now client has been closed // Now client has been closed
@@ -154,10 +150,10 @@ func (c *Client) Close() error {
c.sessionsLock.Lock() c.sessionsLock.Lock()
sessionToClose := make([]*Session, 0, len(c.sessions)) sessionToClose := make([]*Session, 0, len(c.sessions))
for seq, session := range c.sessions { for _, session := range c.sessions {
sessionToClose = append(sessionToClose, session) sessionToClose = append(sessionToClose, session)
delete(c.sessions, seq)
} }
c.sessions = make(map[uint64]*Session)
c.sessionsLock.Unlock() c.sessionsLock.Unlock()
for _, session := range sessionToClose { for _, session := range sessionToClose {

View File

@@ -9,9 +9,14 @@ const ( // cmds
cmdSYN = 1 // stream open cmdSYN = 1 // stream open
cmdPSH = 2 // data push cmdPSH = 2 // data push
cmdFIN = 3 // stream close, a.k.a EOF mark cmdFIN = 3 // stream close, a.k.a EOF mark
cmdSettings = 4 // Settings cmdSettings = 4 // Settings (Client send to Server)
cmdAlert = 5 // Alert cmdAlert = 5 // Alert
cmdUpdatePaddingScheme = 6 // update padding scheme cmdUpdatePaddingScheme = 6 // update padding scheme
// Since version 2
cmdSYNACK = 7 // Server reports to the client that the stream has been opened
cmdHeartRequest = 8 // Keep alive command
cmdHeartResponse = 9 // Keep alive command
cmdServerSettings = 10 // Settings (Server send to client)
) )
const ( const (

View File

@@ -3,9 +3,11 @@ package session
import ( import (
"crypto/md5" "crypto/md5"
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"net" "net"
"runtime/debug" "runtime/debug"
"strconv"
"sync" "sync"
"time" "time"
@@ -30,11 +32,16 @@ type Session struct {
die chan struct{} die chan struct{}
dieHook func() dieHook func()
synDone func()
synDoneLock sync.Mutex
// pool // pool
seq uint64 seq uint64
idleSince time.Time idleSince time.Time
padding *atomic.TypedValue[*padding.PaddingFactory] padding *atomic.TypedValue[*padding.PaddingFactory]
peerVersion byte
// client // client
isClient bool isClient bool
sendPadding bool sendPadding bool
@@ -76,7 +83,7 @@ func (s *Session) Run() {
} }
settings := util.StringMap{ settings := util.StringMap{
"v": "1", "v": "2",
"client": "mihomo/" + constant.Version, "client": "mihomo/" + constant.Version,
"padding-md5": s.padding.Load().Md5, "padding-md5": s.padding.Load().Md5,
} }
@@ -105,15 +112,16 @@ func (s *Session) Close() error {
close(s.die) close(s.die)
once = true once = true
}) })
if once { if once {
if s.dieHook != nil { if s.dieHook != nil {
s.dieHook() s.dieHook()
s.dieHook = nil
} }
s.streamLock.Lock() s.streamLock.Lock()
for k := range s.streams { for _, stream := range s.streams {
s.streams[k].sessionClose() stream.Close()
} }
s.streams = make(map[uint32]*Stream)
s.streamLock.Unlock() s.streamLock.Unlock()
return s.conn.Close() return s.conn.Close()
} else { } else {
@@ -132,6 +140,17 @@ func (s *Session) OpenStream() (*Stream, error) {
//logrus.Debugln("stream open", sid, s.streams) //logrus.Debugln("stream open", sid, s.streams)
if sid >= 2 && s.peerVersion >= 2 {
s.synDoneLock.Lock()
if s.synDone != nil {
s.synDone()
}
s.synDone = util.NewDeadlineWatcher(time.Second*3, func() {
s.Close()
})
s.synDoneLock.Unlock()
}
if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil {
return nil, err return nil, err
} }
@@ -195,13 +214,37 @@ func (s *Session) recvLoop() error {
if _, ok := s.streams[sid]; !ok { if _, ok := s.streams[sid]; !ok {
stream := newStream(sid, s) stream := newStream(sid, s)
s.streams[sid] = stream s.streams[sid] = stream
if s.onNewStream != nil { go func() {
go s.onNewStream(stream) if s.onNewStream != nil {
} else { s.onNewStream(stream)
go s.Close() } else {
} stream.Close()
}
}()
} }
s.streamLock.Unlock() s.streamLock.Unlock()
case cmdSYNACK: // should be client only
s.synDoneLock.Lock()
if s.synDone != nil {
s.synDone()
s.synDone = nil
}
s.synDoneLock.Unlock()
if hdr.Length() > 0 {
buffer := pool.Get(int(hdr.Length()))
if _, err := io.ReadFull(s.conn, buffer); err != nil {
pool.Put(buffer)
return err
}
// report error
s.streamLock.RLock()
stream, ok := s.streams[sid]
s.streamLock.RUnlock()
if ok {
stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer)))
}
pool.Put(buffer)
}
case cmdFIN: case cmdFIN:
s.streamLock.RLock() s.streamLock.RLock()
stream, ok := s.streams[sid] stream, ok := s.streams[sid]
@@ -240,6 +283,20 @@ func (s *Session) recvLoop() error {
return err return err
} }
} }
// check client's version
if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 {
s.peerVersion = byte(v)
// send cmdServerSettings
f := newFrame(cmdServerSettings, 0)
f.data = util.StringMap{
"v": "2",
}.ToBytes()
_, err = s.writeFrame(f)
if err != nil {
pool.Put(buffer)
return err
}
}
} }
pool.Put(buffer) pool.Put(buffer)
} }
@@ -265,12 +322,35 @@ func (s *Session) recvLoop() error {
} }
if s.isClient { if s.isClient {
if padding.UpdatePaddingScheme(rawScheme, s.padding) { if padding.UpdatePaddingScheme(rawScheme, s.padding) {
log.Infoln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) log.Debugln("[Update padding succeed] %x\n", md5.Sum(rawScheme))
} else { } else {
log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme)) log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme))
} }
} }
} }
case cmdHeartRequest:
if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil {
return err
}
case cmdHeartResponse:
// Active keepalive checking is not implemented yet
break
case cmdServerSettings:
if hdr.Length() > 0 {
buffer := pool.Get(int(hdr.Length()))
if _, err := io.ReadFull(s.conn, buffer); err != nil {
pool.Put(buffer)
return err
}
if s.isClient {
// check server's version
m := util.StringMapFromBytes(buffer)
if v, err := strconv.Atoi(m["v"]); err == nil {
s.peerVersion = byte(v)
}
}
pool.Put(buffer)
}
default: default:
// I don't know what command it is (can't have data) // I don't know what command it is (can't have data)
} }
@@ -280,8 +360,10 @@ func (s *Session) recvLoop() error {
} }
} }
// notify the session that a stream has closed
func (s *Session) streamClosed(sid uint32) error { func (s *Session) streamClosed(sid uint32) error {
if s.IsClosed() {
return io.ErrClosedPipe
}
_, err := s.writeFrame(newFrame(cmdFIN, sid)) _, err := s.writeFrame(newFrame(cmdFIN, sid))
s.streamLock.Lock() s.streamLock.Lock()
delete(s.streams, sid) delete(s.streams, sid)

View File

@@ -22,6 +22,9 @@ type Stream struct {
dieOnce sync.Once dieOnce sync.Once
dieHook func() dieHook func()
dieErr error
reportOnce sync.Once
} }
// newStream initiates a Stream struct // newStream initiates a Stream struct
@@ -36,7 +39,11 @@ func newStream(id uint32, sess *Session) *Stream {
// Read implements net.Conn // Read implements net.Conn
func (s *Stream) Read(b []byte) (n int, err error) { func (s *Stream) Read(b []byte) (n int, err error) {
return s.pipeR.Read(b) n, err = s.pipeR.Read(b)
if s.dieErr != nil {
err = s.dieErr
}
return
} }
// Write implements net.Conn // Write implements net.Conn
@@ -54,25 +61,28 @@ func (s *Stream) Write(b []byte) (n int, err error) {
// Close implements net.Conn // Close implements net.Conn
func (s *Stream) Close() error { func (s *Stream) Close() error {
if s.sessionClose() { return s.CloseWithError(io.ErrClosedPipe)
// notify remote
return s.sess.streamClosed(s.id)
} else {
return io.ErrClosedPipe
}
} }
// sessionClose close stream from session side, do not notify remote func (s *Stream) CloseWithError(err error) error {
func (s *Stream) sessionClose() (once bool) { // if err != io.ErrClosedPipe {
// logrus.Debugln(err)
// }
var once bool
s.dieOnce.Do(func() { s.dieOnce.Do(func() {
s.dieErr = err
s.pipeR.Close() s.pipeR.Close()
once = true once = true
})
if once {
if s.dieHook != nil { if s.dieHook != nil {
s.dieHook() s.dieHook()
s.dieHook = nil s.dieHook = nil
} }
}) return s.sess.streamClosed(s.id)
return } else {
return s.dieErr
}
} }
func (s *Stream) SetReadDeadline(t time.Time) error { func (s *Stream) SetReadDeadline(t time.Time) error {
@@ -108,3 +118,33 @@ func (s *Stream) RemoteAddr() net.Addr {
} }
return nil return nil
} }
// HandshakeFailure should be called when Server fail to create outbound proxy
func (s *Stream) HandshakeFailure(err error) error {
var once bool
s.reportOnce.Do(func() {
once = true
})
if once && err != nil && s.sess.peerVersion >= 2 {
f := newFrame(cmdSYNACK, s.id)
f.data = []byte(err.Error())
if _, err := s.sess.writeFrame(f); err != nil {
return err
}
}
return nil
}
// HandshakeSuccess should be called when Server success to create outbound proxy
func (s *Stream) HandshakeSuccess() error {
var once bool
s.reportOnce.Do(func() {
once = true
})
if once && s.sess.peerVersion >= 2 {
if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,25 @@
package util
import (
"sync"
"time"
)
func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) {
t := time.NewTimer(ddl)
closeCh := make(chan struct{})
go func() {
defer t.Stop()
select {
case <-closeCh:
case <-t.C:
timeOut()
}
}()
var once sync.Once
return func() {
once.Do(func() {
close(closeCh)
})
}
}

View File

@@ -16,7 +16,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '22'
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
@@ -57,7 +57,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '22'
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm

View File

@@ -34,7 +34,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '22'
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm

View File

@@ -53,7 +53,7 @@
"@csstools/normalize.css": "12.1.1", "@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5", "@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"@iconify/json": "2.2.319", "@iconify/json": "2.2.321",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.69.0", "@tanstack/react-query": "5.69.0",
"@tanstack/react-router": "1.114.27", "@tanstack/react-router": "1.114.27",

View File

@@ -5,7 +5,7 @@
"mihomo_alpha": "alpha-7b38261", "mihomo_alpha": "alpha-7b38261",
"clash_rs": "v0.7.6", "clash_rs": "v0.7.6",
"clash_premium": "2023-09-05-gdcc8d87", "clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.7.6-alpha+sha.153fb70" "clash_rs_alpha": "0.7.6-alpha+sha.fa04093"
}, },
"arch_template": { "arch_template": {
"mihomo": { "mihomo": {
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
} }
}, },
"updated_at": "2025-03-24T22:21:04.112Z" "updated_at": "2025-03-26T22:20:53.137Z"
} }

View File

@@ -333,8 +333,8 @@ importers:
specifier: 11.14.0 specifier: 11.14.0
version: 11.14.0(@types/react@19.0.12)(react@19.0.0) version: 11.14.0(@types/react@19.0.12)(react@19.0.0)
'@iconify/json': '@iconify/json':
specifier: 2.2.319 specifier: 2.2.321
version: 2.2.319 version: 2.2.321
'@monaco-editor/react': '@monaco-editor/react':
specifier: 4.7.0 specifier: 4.7.0
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -1674,8 +1674,8 @@ packages:
'@vue/compiler-sfc': '@vue/compiler-sfc':
optional: true optional: true
'@iconify/json@2.2.319': '@iconify/json@2.2.321':
resolution: {integrity: sha512-ZGX8O3PXDxXdgltuW2JlXa7IuZ6uc34qKVIBRyPNo63fxjbw7rSOox7HKi3fJyhXqoL3aN0AtK1yOb5Cgcte8w==} resolution: {integrity: sha512-0D1OjRK77jD7dhrb4IhGiBTqLufi6I6HaYso6qkSkvm0WqbWgzGnoNEpw+g/jzSJAiLfuBwOGz6b7Q/ZJqsYrw==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -9450,7 +9450,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@iconify/json@2.2.319': '@iconify/json@2.2.321':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
pathe: 1.1.2 pathe: 1.1.2

View File

@@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
# 运行 clippy # 运行 clippy
cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix # cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
# 如果 clippy 失败,阻止 push # 如果 clippy 失败,阻止 push
if [ $? -ne 0 ]; then # if [ $? -ne 0 ]; then
echo "Clippy found issues in sub_crate. Please fix them before pushing." # echo "Clippy found issues in sub_crate. Please fix them before pushing."
exit 1 # exit 1
fi # fi
# 允许 push # 允许 push
exit 0 exit 0

View File

@@ -2,14 +2,24 @@
#### 已知问题 #### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优 - 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸可能会导致不正常图标和速率间隙
### 2.2.3-alpha 相对于 2.2.2 ### 2.2.3-alpha 相对于 2.2.2
#### 修复了: #### 修复了:
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题 - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开启自启”和“DNS覆写”开关跳动问题
- 自定义托盘图标未能应用更改
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- MacOS 托盘速率显示不全
#### 优化 #### 新增了:
- 重构了内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 - ClashVergeRev 从现在开始不再强依赖系统服务和管理权限
- 集中管理应用数据,优化数据获取和刷新逻辑
#### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
- 优化首页流量图表代码,调整图表文字边距
- MacOS 托盘速率更好的显示样式和更新逻辑
## v2.2.2 ## v2.2.2

View File

@@ -276,17 +276,6 @@ dependencies = [
"futures-lite 1.13.0", "futures-lite 1.13.0",
] ]
[[package]]
name = "async-fs"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
dependencies = [
"async-lock 3.4.0",
"blocking",
"futures-lite 2.6.0",
]
[[package]] [[package]]
name = "async-io" name = "async-io"
version = "1.13.0" version = "1.13.0"
@@ -374,25 +363,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "async-process"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
dependencies = [
"async-channel 2.3.1",
"async-io 2.4.0",
"async-lock 3.4.0",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.3.0",
"futures-lite 2.6.0",
"rustix 0.38.44",
"tracing",
]
[[package]] [[package]]
name = "async-recursion" name = "async-recursion"
version = "1.1.1" version = "1.1.1"
@@ -1118,7 +1088,6 @@ dependencies = [
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-global-shortcut", "tauri-plugin-global-shortcut",
"tauri-plugin-notification",
"tauri-plugin-process", "tauri-plugin-process",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
@@ -1761,16 +1730,6 @@ dependencies = [
"dirs-sys 0.5.0", "dirs-sys 0.5.0",
] ]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.3.7" version = "0.3.7"
@@ -1794,17 +1753,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users 0.4.6",
"winapi",
]
[[package]] [[package]]
name = "dispatch" name = "dispatch"
version = "0.2.0" version = "0.2.0"
@@ -3795,19 +3743,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mac-notification-sys"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91"
dependencies = [
"cc",
"dirs-next",
"objc-foundation",
"objc_id",
"time",
]
[[package]] [[package]]
name = "malloc_buf" name = "malloc_buf"
version = "0.0.6" version = "0.0.6"
@@ -4195,20 +4130,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "notify-rust"
version = "4.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fa3b9f2364a09bd359aa0206702882e208437450866a374d5372d64aece4029"
dependencies = [
"futures-lite 2.6.0",
"log",
"mac-notification-sys",
"serde",
"tauri-winrt-notification",
"zbus",
]
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.1" version = "0.4.1"
@@ -4370,17 +4291,6 @@ dependencies = [
"malloc_buf", "malloc_buf",
] ]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.3.5" version = "0.3.5"
@@ -4645,15 +4555,6 @@ dependencies = [
"objc2-foundation 0.3.0", "objc2-foundation 0.3.0",
] ]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.36.7"
@@ -5411,15 +5312,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.32.0" version = "0.32.0"
@@ -6570,11 +6462,11 @@ checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1"
dependencies = [ dependencies = [
"async-channel 1.9.0", "async-channel 1.9.0",
"async-executor", "async-executor",
"async-fs 1.6.0", "async-fs",
"async-io 1.13.0", "async-io 1.13.0",
"async-lock 2.8.0", "async-lock 2.8.0",
"async-net", "async-net",
"async-process 1.8.1", "async-process",
"blocking", "blocking",
"futures-lite 1.13.0", "futures-lite 1.13.0",
] ]
@@ -7176,25 +7068,6 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
[[package]]
name = "tauri-plugin-notification"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c474c7cc524385e682ccc1e149e13913a66fd8586ac4c2319cf01b78f070d309"
dependencies = [
"log",
"notify-rust",
"rand 0.8.5",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"time",
"url",
]
[[package]] [[package]]
name = "tauri-plugin-process" name = "tauri-plugin-process"
version = "2.2.0" version = "2.2.0"
@@ -7369,17 +7242,6 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "tauri-winrt-notification"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871"
dependencies = [
"quick-xml 0.31.0",
"windows 0.56.0",
"windows-version",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.19.1" version = "3.19.1"
@@ -8721,16 +8583,6 @@ dependencies = [
"windows-version", "windows-version",
] ]
[[package]]
name = "windows"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
dependencies = [
"windows-core 0.56.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.57.0" version = "0.57.0"
@@ -8782,18 +8634,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-core"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
dependencies = [
"windows-implement 0.56.0",
"windows-interface 0.56.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.57.0" version = "0.57.0"
@@ -8842,17 +8682,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-implement"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.57.0" version = "0.57.0"
@@ -8886,17 +8715,6 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "windows-interface"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.57.0" version = "0.57.0"
@@ -9553,15 +9371,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor",
"async-fs 2.1.2",
"async-io 2.4.0",
"async-lock 3.4.0",
"async-process 2.3.0",
"async-recursion", "async-recursion",
"async-task",
"async-trait", "async-trait",
"blocking",
"enumflags2", "enumflags2",
"event-listener 5.3.0", "event-listener 5.3.0",
"futures-core", "futures-core",

View File

@@ -35,10 +35,10 @@ delay_timer = "0.11.6"
parking_lot = "0.12" parking_lot = "0.12"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
tokio = { version = "1.44", features = [ tokio = { version = "1.44", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
@@ -47,17 +47,16 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d74
image = "0.25.6" image = "0.25.6"
imageproc = "0.25.0" imageproc = "0.25.0"
tauri = { version = "2.4.0", features = [ tauri = { version = "2.4.0", features = [
"protocol-asset", "protocol-asset",
"devtools", "devtools",
"tray-icon", "tray-icon",
"image-ico", "image-ico",
"image-png", "image-png",
] } ] }
network-interface = { version = "2.0.0", features = ["serde"] } network-interface = { version = "2.0.0", features = ["serde"] }
tauri-plugin-shell = "2.2.0" tauri-plugin-shell = "2.2.0"
tauri-plugin-dialog = "2.2.0" tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0" tauri-plugin-fs = "2.2.0"
tauri-plugin-notification = "2.2.2"
tauri-plugin-process = "2.2.0" tauri-plugin-process = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.2" tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-deep-link = "2.2.0" tauri-plugin-deep-link = "2.2.0"

View File

@@ -68,7 +68,6 @@
"shell:allow-spawn", "shell:allow-spawn",
"shell:allow-stdin-write", "shell:allow-stdin-write",
"dialog:allow-open", "dialog:allow-open",
"notification:default",
"global-shortcut:allow-is-registered", "global-shortcut:allow-is-registered",
"global-shortcut:allow-register", "global-shortcut:allow-register",
"global-shortcut:allow-register-all", "global-shortcut:allow-register-all",
@@ -79,7 +78,6 @@
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"shell:default", "shell:default",
"dialog:default", "dialog:default"
"notification:default"
] ]
} }

View File

@@ -1,5 +1,9 @@
use super::CmdResult; use super::CmdResult;
use crate::{feat, utils::dirs, wrap_err}; use crate::{
feat, logging,
utils::{dirs, logging::Type},
wrap_err,
};
use tauri::Manager; use tauri::Manager;
/// 打开应用程序所在目录 /// 打开应用程序所在目录
@@ -194,7 +198,14 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
) )
.unwrap_or_default(); .unwrap_or_default();
} }
logging!(
info,
Type::CMD,
true,
"Copying icon file path: {:?} -> file dist: {:?}",
path,
dest_path
);
match fs::copy(file_path, &dest_path) { match fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()), Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),

View File

@@ -2,8 +2,8 @@ use super::CmdResult;
use crate::{ use crate::{
config::*, config::*,
core::*, core::*,
feat, log_err, ret_err, feat, log_err, logging, ret_err,
utils::{dirs, help}, utils::{dirs, help, logging::Type},
wrap_err, wrap_err,
}; };
@@ -77,20 +77,19 @@ pub async fn delete_profile(index: String) -> CmdResult {
/// 修改profiles的配置 /// 修改profiles的配置
#[tauri::command] #[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> { pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
println!("[cmd配置patch] 开始修改配置文件"); logging!(info, Type::CMD, true, "开始修改配置文件");
// 保存当前配置,以便在验证失败时恢复 // 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest().current.clone(); let current_profile = Config::profiles().latest().current.clone();
println!("[cmd配置patch] 当前配置: {:?}", current_profile); logging!(info, Type::CMD, true, "当前配置: {:?}", current_profile);
// 更新profiles配置 // 更新profiles配置
println!("[cmd配置patch] 正在更新配置草稿"); logging!(info, Type::CMD, true, "正在更新配置草稿");
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?; wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
// 更新配置并进行验证 // 更新配置并进行验证
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok((true, _)) => { Ok((true, _)) => {
println!("[cmd配置patch] 配置更新成功"); logging!(info, Type::CMD, true, "配置更新成功");
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
let _ = tray::Tray::global().update_tooltip(); let _ = tray::Tray::global().update_tooltip();
Config::profiles().apply(); Config::profiles().apply();
@@ -98,12 +97,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
Ok(true) Ok(true)
} }
Ok((false, error_msg)) => { Ok((false, error_msg)) => {
println!("[cmd配置patch] 配置验证失败: {}", error_msg); logging!(warn, Type::CMD, true, "配置验证失败: {}", error_msg);
Config::profiles().discard(); Config::profiles().discard();
// 如果验证失败,恢复到之前的配置 // 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile { if let Some(prev_profile) = current_profile {
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile); logging!(
info,
Type::CMD,
true,
"尝试恢复到之前的配置: {}",
prev_profile
);
let restore_profiles = IProfiles { let restore_profiles = IProfiles {
current: Some(prev_profile), current: Some(prev_profile),
items: None, items: None,
@@ -112,7 +116,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?; wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
Config::profiles().apply(); Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?; wrap_err!(Config::profiles().data().save_file())?;
println!("[cmd配置patch] 成功恢复到之前的配置"); logging!(info, Type::CMD, true, "成功恢复到之前的配置");
} }
// 发送验证错误通知 // 发送验证错误通知
@@ -120,7 +124,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
Ok(false) Ok(false)
} }
Err(e) => { Err(e) => {
println!("[cmd配置patch] 更新过程发生错误: {}", e); logging!(warn, Type::CMD, true, "更新过程发生错误: {}", e);
Config::profiles().discard(); Config::profiles().discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string()); handle::Handle::notice_message("config_validate::boot_error", e.to_string());
Ok(false) Ok(false)

View File

@@ -4,21 +4,21 @@ use crate::module::mihomo::MihomoManager;
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let mannager = MihomoManager::global(); let mannager = MihomoManager::global();
let proxies = mannager
mannager
.refresh_proxies() .refresh_proxies()
.await .await
.map(|_| mannager.get_proxies()) .map(|_| mannager.get_proxies())
.or_else(|_| Ok(mannager.get_proxies())); .or_else(|_| Ok(mannager.get_proxies()))
proxies
} }
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let mannager = MihomoManager::global(); let mannager = MihomoManager::global();
let providers = mannager
mannager
.refresh_providers_proxies() .refresh_providers_proxies()
.await .await
.map(|_| mannager.get_providers_proxies()) .map(|_| mannager.get_providers_proxies())
.or_else(|_| Ok(mannager.get_providers_proxies())); .or_else(|_| Ok(mannager.get_providers_proxies()))
providers
} }

View File

@@ -4,9 +4,9 @@ use crate::{
config::*, config::*,
core::{ core::{
handle, handle,
service::{self, is_service_available}, service::{self},
}, },
log_err, logging, logging_error, logging, logging_error,
module::mihomo::MihomoManager, module::mihomo::MihomoManager,
utils::{ utils::{
dirs, dirs,
@@ -63,7 +63,14 @@ impl CoreManager {
let content = match std::fs::read_to_string(path) { let content = match std::fs::read_to_string(path) {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err); logging!(
warn,
Type::Config,
true,
"无法读取文件以检测类型: {}, 错误: {}",
path,
err
);
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Failed to read file to detect type: {}", "Failed to read file to detect type: {}",
err err
@@ -114,7 +121,13 @@ impl CoreManager {
} }
// 默认情况:无法确定时,假设为非脚本文件(更安全) // 默认情况:无法确定时,假设为非脚本文件(更安全)
log::debug!(target: "app", "无法确定文件类型默认当作YAML处理: {}", path); logging!(
debug,
Type::Config,
true,
"无法确定文件类型默认当作YAML处理: {}",
path
);
Ok(false) Ok(false)
} }
/// 使用默认配置 /// 使用默认配置
@@ -147,7 +160,7 @@ impl CoreManager {
) -> Result<(bool, String)> { ) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证 // 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
println!("[core配置验证] 应用正在退出,跳过验证"); logging!(info, Type::Core, true, "应用正在退出,跳过验证");
return Ok((true, String::new())); return Ok((true, String::new()));
} }
@@ -160,8 +173,11 @@ impl CoreManager {
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证 // 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
if is_merge_file.unwrap_or(false) { if is_merge_file.unwrap_or(false) {
println!( logging!(
"[core配置验证] 检测到Merge文件仅进行语法检查: {}", info,
Type::Config,
true,
"检测到Merge文件仅进行语法检查: {}",
config_path config_path
); );
return self.validate_file_syntax(config_path).await; return self.validate_file_syntax(config_path).await;
@@ -175,19 +191,38 @@ impl CoreManager {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(err) => {
// 如果无法确定文件类型尝试使用Clash内核验证 // 如果无法确定文件类型尝试使用Clash内核验证
log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err); logging!(
warn,
Type::Config,
true,
"无法确定文件类型: {}, 错误: {}",
config_path,
err
);
return self.validate_config_internal(config_path).await; return self.validate_config_internal(config_path).await;
} }
} }
}; };
if is_script { if is_script {
log::info!(target: "app", "检测到脚本文件使用JavaScript验证: {}", config_path); logging!(
info,
Type::Config,
true,
"检测到脚本文件使用JavaScript验证: {}",
config_path
);
return self.validate_script_file(config_path).await; return self.validate_script_file(config_path).await;
} }
// 对YAML配置文件使用Clash内核验证 // 对YAML配置文件使用Clash内核验证
log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path); logging!(
info,
Type::Config,
true,
"使用Clash内核验证配置文件: {}",
config_path
);
self.validate_config_internal(config_path).await self.validate_config_internal(config_path).await
} }
/// 内部验证配置文件的实现 /// 内部验证配置文件的实现
@@ -234,7 +269,7 @@ impl CoreManager {
logging!(info, Type::Config, true, "-------- 验证结果 --------"); logging!(info, Type::Config, true, "-------- 验证结果 --------");
if !stderr.is_empty() { if !stderr.is_empty() {
logging!(info, Type::Core, true, "stderr输出:\n{}", stderr); logging!(info, Type::Config, true, "stderr输出:\n{}", stderr);
} }
if has_error { if has_error {
@@ -259,29 +294,28 @@ impl CoreManager {
} }
/// 只进行文件语法检查,不进行完整验证 /// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> { async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
println!("[core配置语法检查] 开始检查文件: {}", config_path); logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
// 读取文件内容 // 读取文件内容
let content = match std::fs::read_to_string(config_path) { let content = match std::fs::read_to_string(config_path) {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
let error_msg = format!("Failed to read file: {}", err); let error_msg = format!("Failed to read file: {}", err);
println!("[core配置语法检查] 无法读取文件: {}", error_msg); logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
}; };
// 对YAML文件尝试解析只检查语法正确性 // 对YAML文件尝试解析只检查语法正确性
println!("[core配置语法检查] 进行YAML语法检查"); logging!(info, Type::Config, true, "进行YAML语法检查");
match serde_yaml::from_str::<serde_yaml::Value>(&content) { match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => { Ok(_) => {
println!("[core配置语法检查] YAML语法检查通过"); logging!(info, Type::Config, true, "YAML语法检查通过");
Ok((true, String::new())) Ok((true, String::new()))
} }
Err(err) => { Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别 // 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {}", err); let error_msg = format!("YAML syntax error: {}", err);
println!("[core配置语法检查] YAML语法错误: {}", error_msg); logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
Ok((false, error_msg)) Ok((false, error_msg))
} }
} }
@@ -293,13 +327,13 @@ impl CoreManager {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
let error_msg = format!("Failed to read script file: {}", err); let error_msg = format!("Failed to read script file: {}", err);
log::warn!(target: "app", "脚本语法错误: {}", err); logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
}; };
log::debug!(target: "app", "验证脚本文件: {}", path); logging!(debug, Type::Config, true, "验证脚本文件: {}", path);
// 使用boa引擎进行基本语法检查 // 使用boa引擎进行基本语法检查
use boa_engine::{Context, Source}; use boa_engine::{Context, Source};
@@ -309,7 +343,7 @@ impl CoreManager {
match result { match result {
Ok(_) => { Ok(_) => {
log::debug!(target: "app", "脚本语法验证通过: {}", path); logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path);
// 检查脚本是否包含main函数 // 检查脚本是否包含main函数
if !content.contains("function main") if !content.contains("function main")
@@ -317,7 +351,7 @@ impl CoreManager {
&& !content.contains("let main") && !content.contains("let main")
{ {
let error_msg = "Script must contain a main function"; let error_msg = "Script must contain a main function";
log::warn!(target: "app", "脚本缺少main函数: {}", path); logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path);
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg); //handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
return Ok((false, error_msg.to_string())); return Ok((false, error_msg.to_string()));
} }
@@ -326,7 +360,7 @@ impl CoreManager {
} }
Err(err) => { Err(err) => {
let error_msg = format!("Script syntax error: {}", err); let error_msg = format!("Script syntax error: {}", err);
log::warn!(target: "app", "脚本语法错误: {}", err); logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg)) Ok((false, error_msg))
} }
@@ -336,39 +370,45 @@ impl CoreManager {
pub async fn update_config(&self) -> Result<(bool, String)> { pub async fn update_config(&self) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过完整验证流程 // 检查程序是否正在退出,如果是则跳过完整验证流程
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
println!("[core配置更新] 应用正在退出,跳过验证"); logging!(info, Type::Config, true, "应用正在退出,跳过验证");
return Ok((true, String::new())); return Ok((true, String::new()));
} }
println!("[core配置更新] 开始更新配置"); logging!(info, Type::Config, true, "开始更新配置");
// 1. 先生成新的配置内容 // 1. 先生成新的配置内容
println!("[core配置更新] 生成新的配置内容"); logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate().await?; Config::generate().await?;
// 2. 生成临时文件并进行验证 // 2. 生成临时文件并进行验证
println!("[core配置更新] 生成临时配置文件用于验证"); logging!(info, Type::Config, true, "生成临时配置文件用于验证");
let temp_config = Config::generate_file(ConfigType::Check)?; let temp_config = Config::generate_file(ConfigType::Check)?;
let temp_config = dirs::path_to_str(&temp_config)?; let temp_config = dirs::path_to_str(&temp_config)?;
println!("[core配置更新] 临时配置文件路径: {}", temp_config); logging!(
info,
Type::Config,
true,
"临时配置文件路径: {}",
temp_config
);
// 3. 验证配置 // 3. 验证配置
match self.validate_config().await { match self.validate_config().await {
Ok((true, _)) => { Ok((true, _)) => {
println!("[core配置更新] 配置验证通过"); logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置 // 4. 验证通过后,生成正式的运行时配置
println!("[core配置更新] 生成运行时配置"); logging!(info, Type::Config, true, "生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run)?; let run_path = Config::generate_file(ConfigType::Run)?;
logging_error!(Type::Core, true, self.put_configs_force(run_path).await); logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
Ok((true, "something".into())) Ok((true, "something".into()))
} }
Ok((false, error_msg)) => { Ok((false, error_msg)) => {
println!("[core配置更新] 配置验证失败: {}", error_msg); logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
Config::runtime().discard(); Config::runtime().discard();
Ok((false, error_msg)) Ok((false, error_msg))
} }
Err(e) => { Err(e) => {
println!("[core配置更新] 验证过程发生错误: {}", e); logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
Config::runtime().discard(); Config::runtime().discard();
Err(e) Err(e)
} }
@@ -480,14 +520,10 @@ impl CoreManager {
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core"); logging!(trace, Type::Core, "Initializing core");
if is_service_available().await.is_ok() { self.start_core().await?;
Self::global().start_core_by_service().await?;
} else {
Self::global().start_core_by_sidecar().await?;
}
logging!(trace, Type::Core, "Initied core"); logging!(trace, Type::Core, "Initied core");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
log_err!(Tray::global().subscribe_traffic().await); logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
Ok(()) Ok(())
} }
@@ -503,7 +539,10 @@ impl CoreManager {
/// 启动核心 /// 启动核心
pub async fn start_core(&self) -> Result<()> { pub async fn start_core(&self) -> Result<()> {
if is_service_available().await.is_ok() { if service::is_service_available().await.is_ok() {
if service::check_service_needs_reinstall().await {
service::reinstall_service().await?;
}
self.start_core_by_service().await?; self.start_core_by_service().await?;
} else { } else {
self.start_core_by_sidecar().await?; self.start_core_by_sidecar().await?;

View File

@@ -8,7 +8,7 @@ use crate::{
feat, feat,
module::{lightweight::entry_lightweight_mode, mihomo::Rate}, module::{lightweight::entry_lightweight_mode, mihomo::Rate},
resolve, resolve,
utils::{dirs, i18n::t, resolve::VERSION}, utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
}; };
use anyhow::Result; use anyhow::Result;
@@ -20,10 +20,7 @@ use parking_lot::Mutex;
use parking_lot::RwLock; use parking_lot::RwLock;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub use speed_rate::{SpeedRate, Traffic}; pub use speed_rate::{SpeedRate, Traffic};
#[cfg(target_os = "macos")] use std::fs;
use std::collections::hash_map::DefaultHasher;
#[cfg(target_os = "macos")]
use std::hash::{Hash, Hasher};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use std::sync::Arc; use std::sync::Arc;
use tauri::{ use tauri::{
@@ -35,19 +32,124 @@ use tauri::{
use tokio::sync::broadcast; use tokio::sync::broadcast;
use super::handle; use super::handle;
#[derive(Clone)]
struct TrayState {}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub struct Tray { pub struct Tray {
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>, pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>, shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
is_subscribed: Arc<RwLock<bool>>, is_subscribed: Arc<RwLock<bool>>,
pub icon_hash: Arc<Mutex<Option<u64>>>,
pub icon_cache: Arc<Mutex<Option<Vec<u8>>>>,
pub rate_cache: Arc<Mutex<Option<Rate>>>, pub rate_cache: Arc<Mutex<Option<Rate>>>,
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
pub struct Tray {} pub struct Tray {}
impl TrayState {
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().latest().clone();
let is_common_tray_icon = verge.common_tray_icon.clone().unwrap_or(false);
if is_common_tray_icon {
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
let icon_data = fs::read(common_icon_path).unwrap();
return (true, icon_data);
}
}
#[cfg(target_os = "macos")]
{
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
)
} else {
(
false,
include_bytes!("../../../icons/tray-icon.ico").to_vec(),
)
}
}
#[cfg(not(target_os = "macos"))]
{
(
false,
include_bytes!("../../../icons/tray-icon.ico").to_vec(),
)
}
}
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().latest().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.clone().unwrap_or(false);
if is_sysproxy_tray_icon {
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
let icon_data = fs::read(sysproxy_icon_path).unwrap();
return (true, icon_data);
}
}
#[cfg(target_os = "macos")]
{
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
)
} else {
(
false,
include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
)
}
}
#[cfg(not(target_os = "macos"))]
{
(
false,
include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
)
}
}
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().latest().clone();
let is_tun_tray_icon = verge.tun_tray_icon.clone().unwrap_or(false);
if is_tun_tray_icon {
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
let icon_data = fs::read(tun_icon_path).unwrap();
return (true, icon_data);
}
}
#[cfg(target_os = "macos")]
{
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
)
} else {
(
false,
include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
)
}
}
#[cfg(not(target_os = "macos"))]
{
(
false,
include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
)
}
}
}
impl Tray { impl Tray {
pub fn global() -> &'static Tray { pub fn global() -> &'static Tray {
static TRAY: OnceCell<Tray> = OnceCell::new(); static TRAY: OnceCell<Tray> = OnceCell::new();
@@ -57,8 +159,6 @@ impl Tray {
speed_rate: Arc::new(Mutex::new(None)), speed_rate: Arc::new(Mutex::new(None)),
shutdown_tx: Arc::new(RwLock::new(None)), shutdown_tx: Arc::new(RwLock::new(None)),
is_subscribed: Arc::new(RwLock::new(false)), is_subscribed: Arc::new(RwLock::new(false)),
icon_hash: Arc::new(Mutex::new(None)),
icon_cache: Arc::new(Mutex::new(None)),
rate_cache: Arc::new(Mutex::new(None)), rate_cache: Arc::new(Mutex::new(None)),
}); });
@@ -159,107 +259,29 @@ impl Tray {
} }
/// 更新托盘图标 /// 更新托盘图标
#[allow(unused_variables)]
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> { pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let verge = Config::verge().latest().clone(); let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false); let app_handle = handle::Handle::global().app_handle().unwrap();
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let tray = app_handle.tray_by_id("main").unwrap(); let tray = app_handle.tray_by_id("main").unwrap();
#[cfg(target_os = "macos")] let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); (true, true) => TrayState::get_tun_tray_icon(),
(true, false) => TrayState::get_sysproxy_tray_icon(),
let icon_bytes = if *system_proxy && !*tun_mode { (false, true) => TrayState::get_tun_tray_icon(),
#[cfg(target_os = "macos")] (false, false) => TrayState::get_common_tray_icon(),
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon-sys.ico").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon-tun.ico").to_vec();
if *tun_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true); let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true); let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
let is_colorful = tray_icon == "colorful"; let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let is_colorful = colorful == "colorful";
let icon_hash = {
let mut hasher = DefaultHasher::new();
icon_bytes.clone().hash(&mut hasher);
hasher.finish()
};
let mut icon_hash_guard = self.icon_hash.lock();
let mut icon_bytes_guard = self.icon_cache.lock();
if *icon_hash_guard != Some(icon_hash) {
*icon_hash_guard = Some(icon_hash);
*icon_bytes_guard = Some(icon_bytes.clone());
}
if !enable_tray_speed { if !enable_tray_speed {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes( let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
&(*icon_bytes_guard).clone().unwrap(),
)?));
let _ = tray.set_icon_as_template(!is_colorful); let _ = tray.set_icon_as_template(!is_colorful);
return Ok(()); return Ok(());
} }
@@ -280,16 +302,20 @@ impl Tray {
*rate_guard = rate; *rate_guard = rate;
let bytes = if enable_tray_icon { let bytes = if enable_tray_icon {
Some(icon_bytes_guard.as_ref().unwrap()) Some(icon_bytes)
} else { } else {
None None
}; };
let rate = rate_guard.as_ref(); let rate = rate_guard.as_ref();
let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap(); let rate_bytes = SpeedRate::add_speed_text(is_custom_icon, bytes, rate).unwrap();
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?)); let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful); if !is_custom_icon {
let _ = tray.set_icon_as_template(!is_colorful);
} else {
let _ = tray.set_icon_as_template(false);
}
} }
Ok(()) Ok(())
} }

View File

@@ -77,14 +77,15 @@ impl SpeedRate {
// 分离图标加载和速率渲染 // 分离图标加载和速率渲染
pub fn add_speed_text<'a>( pub fn add_speed_text<'a>(
icon_bytes: Option<&'a Vec<u8>>, is_custom_icon: bool,
icon_bytes: Option<Vec<u8>>,
rate: Option<&'a Rate>, rate: Option<&'a Rate>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 }); let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
let (mut icon_width, mut icon_height) = (0, 256); let (mut icon_width, mut icon_height) = (0, 256);
let icon_image = if let Some(bytes) = icon_bytes { let icon_image = if let Some(bytes) = icon_bytes.clone() {
let icon_image = image::load_from_memory(bytes)?; let icon_image = image::load_from_memory(&bytes)?;
icon_width = icon_image.width(); icon_width = icon_image.width();
icon_height = icon_image.height(); icon_height = icon_image.height();
icon_image icon_image
@@ -94,23 +95,24 @@ impl SpeedRate {
}; };
// 判断是否为彩色图标 // 判断是否为彩色图标
let is_colorful = if let Some(bytes) = icon_bytes { let is_colorful = if let Some(bytes) = icon_bytes.clone() {
!crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false) !crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
} else { } else {
false false
}; };
// 增加文本宽度和间距 let total_width = match (is_custom_icon, icon_bytes.is_some()) {
let total_width = if icon_bytes.is_some() { (true, true) => 510,
if icon_width < 580 { (true, false) => 740,
icon_width + 580 (false, false) => 740,
} else { (false, true) => icon_width + 740,
icon_width
}
} else {
580
}; };
// println!(
// "icon_height: {}, icon_wight: {}, total_width: {}",
// icon_height, icon_width, total_width
// );
// 创建新的透明画布 // 创建新的透明画布
let mut combined_image = RgbaImage::new(total_width, icon_height); let mut combined_image = RgbaImage::new(total_width, icon_height);
@@ -145,17 +147,30 @@ impl SpeedRate {
let scale = ab_glyph::PxScale::from(font_size); let scale = ab_glyph::PxScale::from(font_size);
// 使用更简洁的速率格式 // 使用更简洁的速率格式
let up_text = format_bytes_speed(rate.up); let up_text = format!("{}", format_bytes_speed(rate.up));
let down_text = format_bytes_speed(rate.down); let down_text = format!("{}", format_bytes_speed(rate.down));
// For test rate display
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
// 计算文本位置,确保垂直间距合适 // 计算文本位置,确保垂直间距合适
// 修改文本位置为居右显示 // 修改文本位置为居右显示
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
// 计算右对齐的文本位置 // 计算右对齐的文本位置
let up_text_x = total_width - up_text_width; // let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
let down_text_x = total_width - down_text_width; // let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
// let up_text_x = total_width - up_text_width;
// let down_text_x = total_width - down_text_width;
// 计算左对齐的文本位置
let (up_text_x, down_text_x) = {
if is_custom_icon || icon_bytes.is_some() {
let text_left_offset = 30;
let left_begin = icon_width + text_left_offset;
(left_begin, left_begin)
} else {
(icon_width, icon_width)
}
};
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小 // 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
let text_height = font_size as i32; let text_height = font_size as i32;

View File

@@ -110,7 +110,6 @@ pub fn run() {
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())

View File

@@ -77,9 +77,39 @@ pub fn app_profiles_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("profiles")) Ok(app_home_dir()?.join("profiles"))
} }
/// icons dir
pub fn app_icons_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("icons"))
}
pub fn find_target_icons(target: &str) -> Result<Option<String>> {
let icons_dir = app_icons_dir()?;
let mut matching_files = Vec::new();
for entry in fs::read_dir(icons_dir)? {
let entry = entry?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{
matching_files.push(path);
}
}
}
if matching_files.is_empty() {
Ok(None)
} else {
let first = path_to_str(matching_files.first().unwrap())?;
Ok(Some(first.to_string()))
}
}
/// logs dir /// logs dir
pub fn app_logs_dir() -> Result<PathBuf> { pub fn app_logs_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("logs")) Ok(app_home_dir()?.join("icons"))
} }
pub fn clash_path() -> Result<PathBuf> { pub fn clash_path() -> Result<PathBuf> {

View File

@@ -167,15 +167,16 @@ macro_rules! t {
/// ``` /// ```
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn format_bytes_speed(speed: u64) -> String { pub fn format_bytes_speed(speed: u64) -> String {
if speed < 1024 { const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
format!("{}B/s", speed) let mut size = speed as f64;
} else if speed < 1024 * 1024 { let mut unit_index = 0;
format!("{:.1}KB/s", speed as f64 / 1024.0)
} else if speed < 1024 * 1024 * 1024 { while size >= 1000.0 && unit_index < UNITS.len() - 1 {
format!("{:.1}MB/s", speed as f64 / 1024.0 / 1024.0) size /= 1024.0;
} else { unit_index += 1;
format!("{:.1}GB/s", speed as f64 / 1024.0 / 1024.0 / 1024.0)
} }
format!("{:.1}{}/s", size, UNITS[unit_index])
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View File

@@ -7,6 +7,7 @@ pub enum Type {
Hotkey, Hotkey,
Window, Window,
Config, Config,
CMD,
} }
impl fmt::Display for Type { impl fmt::Display for Type {
@@ -17,6 +18,7 @@ impl fmt::Display for Type {
Type::Hotkey => write!(f, "[Hotkey]"), Type::Hotkey => write!(f, "[Hotkey]"),
Type::Window => write!(f, "[Window]"), Type::Window => write!(f, "[Window]"),
Type::Config => write!(f, "[Config]"), Type::Config => write!(f, "[Config]"),
Type::CMD => write!(f, "[CMD]"),
} }
} }
} }

View File

@@ -18,7 +18,6 @@ use tauri::{App, Manager};
use tauri::Url; use tauri::Url;
//#[cfg(not(target_os = "linux"))] //#[cfg(not(target_os = "linux"))]
// use window_shadows::set_shadow; // use window_shadows::set_shadow;
use tauri_plugin_notification::NotificationExt;
pub static VERSION: OnceCell<String> = OnceCell::new(); pub static VERSION: OnceCell<String> = OnceCell::new();
@@ -239,8 +238,6 @@ pub fn create_window() {
pub async fn resolve_scheme(param: String) -> Result<()> { pub async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {}", param); log::info!(target:"app", "received deep link: {}", param);
let app_handle = handle::Handle::global().app_handle().unwrap();
let param_str = if param.starts_with("[") && param.len() > 4 { let param_str = if param.starts_with("[") && param.len() > 4 {
param param
.get(2..param.len() - 2) .get(2..param.len() - 2)
@@ -280,24 +277,9 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
let uid = item.uid.clone().unwrap(); let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item)); let _ = wrap_err!(Config::profiles().data().append_item(item));
handle::Handle::notice_message("import_sub_url::ok", uid); handle::Handle::notice_message("import_sub_url::ok", uid);
app_handle
.notification()
.builder()
.title("Clash Verge")
.body("Import profile success")
.show()
.unwrap();
} }
Err(e) => { Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string()); handle::Handle::notice_message("import_sub_url::error", e.to_string());
app_handle
.notification()
.builder()
.title("Clash Verge")
.body(format!("Import profile failed: {e}"))
.show()
.unwrap();
} }
} }
} }

View File

@@ -39,69 +39,35 @@ export interface EnhancedTrafficGraphRef {
// 时间范围类型 // 时间范围类型
type TimeRange = 1 | 5 | 10; // 分钟 type TimeRange = 1 | 5 | 10; // 分钟
// 创建一个明确的类型 // 数据点类型
type DataPoint = ITrafficItem & { name: string; timestamp: number }; type DataPoint = ITrafficItem & { name: string; timestamp: number };
// 控制帧率的工具函数
const FPS_LIMIT = 1; // 限制为1fps因为数据每秒才更新一次
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔即1000ms
// 全局存储流量数据历史记录
declare global {
interface Window {
trafficHistoryData?: DataPoint[];
trafficHistoryStyle?: "line" | "area";
trafficHistoryTimeRange?: TimeRange;
}
}
// 初始化全局存储
if (typeof window !== "undefined" && !window.trafficHistoryData) {
window.trafficHistoryData = [];
window.trafficHistoryStyle = "area";
window.trafficHistoryTimeRange = 10;
}
/** /**
* 增强型流量图表组件 * 增强型流量图表组件
* 基于 Recharts 实现,支持线图和面积图两种模式
*/ */
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>( export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
(props, ref) => { (props, ref) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
// 从全局变量恢复状态 // 基础状态
const [timeRange, setTimeRange] = useState<TimeRange>( const [timeRange, setTimeRange] = useState<TimeRange>(10);
window.trafficHistoryTimeRange || 10 const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
);
const [chartStyle, setChartStyle] = useState<"line" | "area">(
window.trafficHistoryStyle || "area"
);
// 使用useRef存储数据避免不必要的重渲染
const dataBufferRef = useRef<DataPoint[]>([]);
// 只为渲染目的的状态
const [displayData, setDisplayData] = useState<DataPoint[]>([]); const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 帧率控制 // 数据缓冲区
const lastUpdateTimeRef = useRef<number>(0); const dataBufferRef = useRef<DataPoint[]>([]);
const pendingUpdateRef = useRef<boolean>(false);
const rafIdRef = useRef<number | null>(null);
// 根据时间范围计算保留的数据点数量 // 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback( const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => { (minutes: TimeRange): number => minutes * 30,
// 使用更低的采样率来减少点的数量每2秒一个点而不是每秒一个点 []
return minutes * 30; // 每分钟30个点(每2秒1个点)
},
[],
); );
// 最大数据点数量 - 基于选择的时间范围 // 最大数据点数量
const MAX_BUFFER_SIZE = useMemo( const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10), () => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange], [getMaxPointsByTimeRange]
); );
// 颜色配置 // 颜色配置
@@ -113,150 +79,51 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
tooltip: theme.palette.background.paper, tooltip: theme.palette.background.paper,
text: theme.palette.text.primary, text: theme.palette.text.primary,
}), }),
[theme], [theme]
); );
// 切换时间范围 // 切换时间范围
const handleTimeRangeClick = useCallback(() => { const handleTimeRangeClick = useCallback(() => {
setTimeRange((prevRange) => { setTimeRange((prevRange) => {
// 在1、5、10分钟之间循环切换 // 在1、5、10分钟之间循环切换
const newRange = prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
window.trafficHistoryTimeRange = newRange; // 保存到全局
return newRange;
}); });
}, []); }, []);
// 初始化数据缓冲区 // 初始化数据缓冲区
useEffect(() => { useEffect(() => {
let initialBuffer: DataPoint[] = []; // 创建初始空数据
const now = Date.now();
// 如果全局有保存的数据,优先使用 const tenMinutesAgo = now - 10 * 60 * 1000;
if (window.trafficHistoryData && window.trafficHistoryData.length > 0) {
initialBuffer = [...window.trafficHistoryData]; const initialBuffer = Array.from(
{ length: MAX_BUFFER_SIZE },
// 确保数据长度符合要求 (_, index) => {
if (initialBuffer.length > MAX_BUFFER_SIZE) { const pointTime =
initialBuffer = initialBuffer.slice(-MAX_BUFFER_SIZE); tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
} else if (initialBuffer.length < MAX_BUFFER_SIZE) { const date = new Date(pointTime);
// 如果历史数据不足,则在前面补充空数据
const now = Date.now(); return {
const oldestTimestamp = initialBuffer.length > 0 up: 0,
? initialBuffer[0].timestamp down: 0,
: now - 10 * 60 * 1000; timestamp: pointTime,
name: date.toLocaleTimeString("en-US", {
const additionalPoints = MAX_BUFFER_SIZE - initialBuffer.length; hour12: false,
const timeInterval = initialBuffer.length > 0 hour: "2-digit",
? (initialBuffer[0].timestamp - (now - 10 * 60 * 1000)) / additionalPoints minute: "2-digit",
: (10 * 60 * 1000) / MAX_BUFFER_SIZE; second: "2-digit",
}),
const emptyPrefix: DataPoint[] = Array.from( };
{ length: additionalPoints },
(_, index) => {
const pointTime = oldestTimestamp - (additionalPoints - index) * timeInterval;
const date = new Date(pointTime);
return {
up: 0,
down: 0,
timestamp: pointTime,
name: date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
};
}
);
initialBuffer = [...emptyPrefix, ...initialBuffer];
} }
} else { );
// 没有历史数据时,创建空的初始缓冲区
const now = Date.now();
const tenMinutesAgo = now - 10 * 60 * 1000;
initialBuffer = Array.from(
{ length: MAX_BUFFER_SIZE },
(_, index) => {
const pointTime =
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
const date = new Date(pointTime);
return {
up: 0,
down: 0,
timestamp: pointTime,
name: date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
};
}
);
}
dataBufferRef.current = initialBuffer; dataBufferRef.current = initialBuffer;
window.trafficHistoryData = initialBuffer; // 保存到全局
// 更新显示数据 // 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow)); setDisplayData(initialBuffer.slice(-pointsToShow));
// 清理函数,取消任何未完成的动画帧
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
};
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]); }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]);
// 处理数据更新并控制帧率的函数
const updateDisplayData = useCallback(() => {
if (pendingUpdateRef.current) {
pendingUpdateRef.current = false;
// 根据当前时间范围计算需要显示的点数
const pointsToShow = getMaxPointsByTimeRange(timeRange);
// 从缓冲区中获取最新的数据点
const newDisplayData = dataBufferRef.current.slice(-pointsToShow);
setDisplayData(newDisplayData);
}
rafIdRef.current = null;
}, [timeRange, getMaxPointsByTimeRange]);
// 节流更新函数
const throttledUpdateData = useCallback(() => {
pendingUpdateRef.current = true;
const now = performance.now();
const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
if (rafIdRef.current === null) {
if (timeSinceLastUpdate >= FRAME_MIN_TIME) {
// 如果距离上次更新已经超过最小帧时间,立即更新
lastUpdateTimeRef.current = now;
rafIdRef.current = requestAnimationFrame(updateDisplayData);
} else {
// 否则,在适当的时间进行更新
const timeToWait = FRAME_MIN_TIME - timeSinceLastUpdate;
setTimeout(() => {
lastUpdateTimeRef.current = performance.now();
rafIdRef.current = requestAnimationFrame(updateDisplayData);
}, timeToWait);
}
}
}, [updateDisplayData]);
// 监听时间范围变化,更新显示数据
useEffect(() => {
throttledUpdateData();
}, [timeRange, throttledUpdateData]);
// 添加数据点方法 // 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => { const appendData = useCallback((data: ITrafficItem) => {
// 安全处理数据 // 安全处理数据
@@ -281,24 +148,24 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
timestamp: timestamp, timestamp: timestamp,
}; };
// 更新ref保持原数组大小 // 更新缓冲区,保持原数组大小
const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer; dataBufferRef.current = newBuffer;
// 保存到全局变量 // 更新显示数据
window.trafficHistoryData = newBuffer; const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow));
// 使用节流更新显示数据 }, [timeRange, getMaxPointsByTimeRange]);
throttledUpdateData();
}, [throttledUpdateData]); // 监听时间范围变化,更新显示数据
useEffect(() => {
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]);
// 切换图表样式 // 切换图表样式
const toggleStyle = useCallback(() => { const toggleStyle = useCallback(() => {
setChartStyle((prev) => { setChartStyle((prev) => prev === "line" ? "area" : "line");
const newStyle = prev === "line" ? "area" : "line";
window.trafficHistoryStyle = newStyle; // 保存到全局
return newStyle;
});
}, []); }, []);
// 暴露方法给父组件 // 暴露方法给父组件
@@ -308,13 +175,12 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
appendData, appendData,
toggleStyle, toggleStyle,
}), }),
[appendData, toggleStyle], [appendData, toggleStyle]
); );
// 格式化工具提示内容 // 格式化工具提示内容
const formatTooltip = useCallback((value: number, name: string, props: any) => { const formatTooltip = useCallback((value: number, name: string, props: any) => {
const [num, unit] = parseTraffic(value); const [num, unit] = parseTraffic(value);
// 使用props.dataKey判断是上传还是下载
return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")]; return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")];
}, [t]); }, [t]);
@@ -327,7 +193,6 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 格式化X轴标签 // 格式化X轴标签
const formatXLabel = useCallback((value: string) => { const formatXLabel = useCallback((value: string) => {
if (!value) return ""; if (!value) return "";
// 只显示小时和分钟
const parts = value.split(":"); const parts = value.split(":");
return `${parts[0]}:${parts[1]}`; return `${parts[0]}:${parts[1]}`;
}, []); }, []);
@@ -352,7 +217,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
isAnimationActive: false, // 禁用动画以减少CPU使用 isAnimationActive: false, // 禁用动画以减少CPU使用
}), []); }), []);
// 曲线类型 - 使用线性曲线避免错位 // 曲线类型
const curveType = "monotone"; const curveType = "monotone";
return ( return (
@@ -386,7 +251,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
tick={{ fontSize: 10, fill: colors.text }} tick={{ fontSize: 10, fill: colors.text }}
tickLine={{ stroke: colors.grid }} tickLine={{ stroke: colors.grid }}
axisLine={{ stroke: colors.grid }} axisLine={{ stroke: colors.grid }}
width={40} width={43}
domain={[0, "auto"]} domain={[0, "auto"]}
/> />
<Tooltip <Tooltip
@@ -470,7 +335,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
tick={{ fontSize: 10, fill: colors.text }} tick={{ fontSize: 10, fill: colors.text }}
tickLine={{ stroke: colors.grid }} tickLine={{ stroke: colors.grid }}
axisLine={{ stroke: colors.grid }} axisLine={{ stroke: colors.grid }}
width={40} width={43}
domain={[0, "auto"]} domain={[0, "auto"]}
padding={{ top: 10, bottom: 0 }} padding={{ top: 10, bottom: 0 }}
/> />

View File

@@ -5,7 +5,6 @@ import {
SettingsRounded, SettingsRounded,
ShuffleRounded, ShuffleRounded,
LanRounded, LanRounded,
DnsRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { DialogRef, Notice, Switch } from "@/components/base"; import { DialogRef, Notice, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
@@ -49,7 +48,16 @@ const SettingClash = ({ onError }: Props) => {
const { enable_random_port = false, verge_mixed_port } = verge ?? {}; const { enable_random_port = false, verge_mixed_port } = verge ?? {};
// 独立跟踪DNS设置开关状态 // 独立跟踪DNS设置开关状态
const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(false); const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => {
// 尝试从localStorage获取之前保存的状态
const savedState = localStorage.getItem("dns_settings_enabled");
if (savedState !== null) {
return savedState === "true";
}
// 如果没有保存的状态则从verge配置中获取
return verge?.enable_dns_settings ?? false;
});
const { addListener } = useListen(); const { addListener } = useListen();
const webRef = useRef<DialogRef>(null); const webRef = useRef<DialogRef>(null);
@@ -59,12 +67,6 @@ const SettingClash = ({ onError }: Props) => {
const networkRef = useRef<DialogRef>(null); const networkRef = useRef<DialogRef>(null);
const dnsRef = useRef<DialogRef>(null); const dnsRef = useRef<DialogRef>(null);
// 初始化时从verge配置中加载DNS设置开关状态
useEffect(() => {
const dnsSettingsState = verge?.enable_dns_settings ?? false;
setDnsSettingsEnabled(dnsSettingsState);
}, [verge]);
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => { const onChangeData = (patch: Partial<IConfigData>) => {
mutateClash((old) => ({ ...(old! || {}), ...patch }), false); mutateClash((old) => ({ ...(old! || {}), ...patch }), false);
@@ -84,15 +86,21 @@ const SettingClash = ({ onError }: Props) => {
// 实现DNS设置开关处理函数 // 实现DNS设置开关处理函数
const handleDnsToggle = useLockFn(async (enable: boolean) => { const handleDnsToggle = useLockFn(async (enable: boolean) => {
try { try {
// 立即更新UI状态
setDnsSettingsEnabled(enable); setDnsSettingsEnabled(enable);
// 保存到localStorage用于记住用户的选择
localStorage.setItem("dns_settings_enabled", String(enable));
// 更新verge配置
await patchVerge({ enable_dns_settings: enable }); await patchVerge({ enable_dns_settings: enable });
await invoke("apply_dns_config", { apply: enable }); await invoke("apply_dns_config", { apply: enable });
setTimeout(() => { setTimeout(() => {
mutateClash(); mutateClash();
}, 500); // 延迟500ms确保后端完成处理 }, 500); // 延迟500ms确保后端完成处理
} catch (err: any) { } catch (err: any) {
Notice.error(err.message || err.toString()); // 如果出错,恢复原始状态
setDnsSettingsEnabled(!enable); setDnsSettingsEnabled(!enable);
localStorage.setItem("dns_settings_enabled", String(!enable));
Notice.error(err.message || err.toString());
await patchVerge({ enable_dns_settings: !enable }).catch(() => { await patchVerge({ enable_dns_settings: !enable }).catch(() => {
// 忽略恢复状态时的错误 // 忽略恢复状态时的错误
}); });
@@ -143,7 +151,6 @@ const SettingClash = ({ onError }: Props) => {
/> />
} }
> >
{/* 使用独立状态不再依赖dns?.enable */}
<Switch <Switch
edge="end" edge="end"
checked={dnsSettingsEnabled} checked={dnsSettingsEnabled}

View File

@@ -43,6 +43,7 @@ const SettingSystem = ({ onError }: Props) => {
const { data: autoLaunchEnabled } = useSWR( const { data: autoLaunchEnabled } = useSWR(
"getAutoLaunchStatus", "getAutoLaunchStatus",
getAutoLaunchStatus, getAutoLaunchStatus,
{ revalidateOnFocus: false }
); );
// 当实际自启动状态与配置不同步时更新配置 // 当实际自启动状态与配置不同步时更新配置

View File

@@ -36,11 +36,96 @@ export default defineConfig({
entry: "monaco-yaml/yaml.worker", entry: "monaco-yaml/yaml.worker",
}, },
], ],
globalAPI: false,
}), }),
], ],
build: { build: {
outDir: "../dist", outDir: "../dist",
emptyOutDir: true, emptyOutDir: true,
target: "esnext",
minify: "terser",
chunkSizeWarningLimit: 4000,
reportCompressedSize: false,
sourcemap: false,
cssCodeSplit: true,
cssMinify: true,
rollupOptions: {
treeshake: {
preset: "recommended",
moduleSideEffects: (id) => !/\.css$/.test(id),
tryCatchDeoptimization: false,
},
output: {
compact: true,
experimentalMinChunkSize: 30000,
dynamicImportInCjs: true,
manualChunks(id) {
if (id.includes("node_modules")) {
// Monaco Editor should be a separate chunk
if (id.includes("monaco-editor")) return "monaco-editor";
// React-related libraries (react, react-dom, react-router-dom, etc.)
if (
id.includes("react") ||
id.includes("react-dom") ||
id.includes("react-router-dom") ||
id.includes("react-transition-group") ||
id.includes("react-error-boundary") ||
id.includes("react-hook-form") ||
id.includes("react-markdown") ||
id.includes("react-virtuoso")
) {
return "react";
}
// Utilities chunk: group commonly used utility libraries
if (
id.includes("axios") ||
id.includes("lodash-es") ||
id.includes("dayjs") ||
id.includes("js-base64") ||
id.includes("js-yaml") ||
id.includes("cli-color") ||
id.includes("nanoid")
) {
return "utils";
}
// Tauri-related plugins: grouping together Tauri plugins
if (
id.includes("@tauri-apps/api") ||
id.includes("@tauri-apps/plugin-clipboard-manager") ||
id.includes("@tauri-apps/plugin-dialog") ||
id.includes("@tauri-apps/plugin-fs") ||
id.includes("@tauri-apps/plugin-global-shortcut") ||
id.includes("@tauri-apps/plugin-notification") ||
id.includes("@tauri-apps/plugin-process") ||
id.includes("@tauri-apps/plugin-shell") ||
id.includes("@tauri-apps/plugin-updater")
) {
return "tauri-plugins";
}
// Material UI libraries (grouped together)
if (
id.includes("@mui/material") ||
id.includes("@mui/icons-material") ||
id.includes("@mui/lab") ||
id.includes("@mui/x-data-grid")
) {
return "mui";
}
// Small vendor packages
const pkg = id.match(/node_modules\/([^\/]+)/)?.[1];
if (pkg && pkg.length < 8) return "small-vendors";
// Large vendor packages
return "large-vendor";
}
},
},
},
}, },
resolve: { resolve: {
alias: { alias: {

View File

@@ -23,14 +23,14 @@ Signed-off-by: Davide Fioravanti <pantanastyle@gmail.com>
obj-$(CONFIG_MTD_SPI_NAND) += spinand.o obj-$(CONFIG_MTD_SPI_NAND) += spinand.o
--- a/drivers/mtd/nand/spi/core.c --- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c +++ b/drivers/mtd/nand/spi/core.c
@@ -940,6 +940,7 @@ static const struct nand_ops spinand_ops @@ -941,6 +941,7 @@ static const struct spinand_manufacturer
static const struct spinand_manufacturer *spinand_manufacturers[] = {
&ato_spinand_manufacturer, &ato_spinand_manufacturer,
&esmt_c8_spinand_manufacturer, &esmt_c8_spinand_manufacturer,
+ &fidelix_spinand_manufacturer,
&etron_spinand_manufacturer, &etron_spinand_manufacturer,
+ &fidelix_spinand_manufacturer,
&gigadevice_spinand_manufacturer, &gigadevice_spinand_manufacturer,
&macronix_spinand_manufacturer, &macronix_spinand_manufacturer,
&micron_spinand_manufacturer,
--- /dev/null --- /dev/null
+++ b/drivers/mtd/nand/spi/fidelix.c +++ b/drivers/mtd/nand/spi/fidelix.c
@@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
@@ -56,8 +56,8 @@ Signed-off-by: Davide Fioravanti <pantanastyle@gmail.com>
+ SPINAND_PROG_LOAD(true, 0, NULL, 0)); + SPINAND_PROG_LOAD(true, 0, NULL, 0));
+ +
+static SPINAND_OP_VARIANTS(update_cache_variants, +static SPINAND_OP_VARIANTS(update_cache_variants,
+ SPINAND_PROG_LOAD_X4(false, 0, NULL, 0), + SPINAND_PROG_LOAD_X4(true, 0, NULL, 0),
+ SPINAND_PROG_LOAD(false, 0, NULL, 0)); + SPINAND_PROG_LOAD(true, 0, NULL, 0));
+ +
+static int fm35x1ga_ooblayout_ecc(struct mtd_info *mtd, int section, +static int fm35x1ga_ooblayout_ecc(struct mtd_info *mtd, int section,
+ struct mtd_oob_region *region) + struct mtd_oob_region *region)

View File

@@ -0,0 +1,133 @@
--- a/drivers/mtd/nand/spi/Makefile
+++ b/drivers/mtd/nand/spi/Makefile
@@ -1,3 +1,3 @@
# SPDX-License-Identifier: GPL-2.0
-spinand-objs := core.o ato.o esmt.o etron.o fidelix.o gigadevice.o macronix.o micron.o paragon.o toshiba.o winbond.o xtx.o
+spinand-objs := core.o ato.o esmt.o etron.o fidelix.o fudanmicro.o gigadevice.o macronix.o micron.o paragon.o toshiba.o winbond.o xtx.o
obj-$(CONFIG_MTD_SPI_NAND) += spinand.o
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -942,6 +942,7 @@ static const struct spinand_manufacturer
&esmt_c8_spinand_manufacturer,
&etron_spinand_manufacturer,
&fidelix_spinand_manufacturer,
+ &fudan_spinand_manufacturer,
&gigadevice_spinand_manufacturer,
&macronix_spinand_manufacturer,
&micron_spinand_manufacturer,
--- /dev/null
+++ b/drivers/mtd/nand/spi/fudanmicro.c
@@ -0,0 +1,103 @@
+#include <linux/device.h>
+#include <linux/kernel.h>
+#include <linux/mtd/spinand.h>
+
+#define SPINAND_MFR_FUDAN 0xA1
+
+#define FM25S01B_STATUS_ECC_MASK (7 << 4)
+#define STATUS_ECC_1_3_BITFLIPS (1 << 4)
+#define STATUS_ECC_4_6_BITFLIPS (3 << 4)
+#define STATUS_ECC_7_8_BITFLIPS (5 << 4)
+
+static SPINAND_OP_VARIANTS(read_cache_variants,
+ SPINAND_PAGE_READ_FROM_CACHE_X4_OP(0, 1, NULL, 0),
+ SPINAND_PAGE_READ_FROM_CACHE_X2_OP(0, 1, NULL, 0),
+ SPINAND_PAGE_READ_FROM_CACHE_OP(true, 0, 1, NULL, 0),
+ SPINAND_PAGE_READ_FROM_CACHE_OP(false, 0, 1, NULL, 0));
+
+static SPINAND_OP_VARIANTS(write_cache_variants,
+ SPINAND_PROG_LOAD_X4(true, 0, NULL, 0),
+ SPINAND_PROG_LOAD(true, 0, NULL, 0));
+
+static SPINAND_OP_VARIANTS(update_cache_variants,
+ SPINAND_PROG_LOAD_X4(false, 0, NULL, 0),
+ SPINAND_PROG_LOAD(false, 0, NULL, 0));
+
+static int fm25s01b_ooblayout_ecc(struct mtd_info *mtd, int section,
+ struct mtd_oob_region *region)
+{
+ if (section)
+ return -ERANGE;
+
+ region->offset = 64;
+ region->length = 64;
+
+ return 0;
+}
+
+static int fm25s01b_ooblayout_free(struct mtd_info *mtd, int section,
+ struct mtd_oob_region *region)
+{
+ if (section > 3)
+ return -ERANGE;
+
+ region->offset = (16 * section) + 4;
+ region->length = 12;
+
+ return 0;
+}
+
+static const struct mtd_ooblayout_ops fm25s01b_ooblayout = {
+ .ecc = fm25s01b_ooblayout_ecc,
+ .free = fm25s01b_ooblayout_free,
+};
+
+static int fm25s01b_ecc_get_status(struct spinand_device *spinand,
+ u8 status)
+{
+ switch (status & FM25S01B_STATUS_ECC_MASK) {
+ case STATUS_ECC_NO_BITFLIPS:
+ return 0;
+
+ case STATUS_ECC_UNCOR_ERROR:
+ return -EBADMSG;
+
+ case STATUS_ECC_1_3_BITFLIPS:
+ return 3;
+
+ case STATUS_ECC_4_6_BITFLIPS:
+ return 6;
+
+ case STATUS_ECC_7_8_BITFLIPS:
+ return 8;
+
+ default:
+ break;
+ }
+
+ return -EINVAL;
+}
+
+static const struct spinand_info fudan_spinand_table[] = {
+ SPINAND_INFO("FM25S01B",
+ SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xD4),
+ NAND_MEMORG(1, 2048, 128, 64, 1024, 20, 1, 1, 1),
+ NAND_ECCREQ(8, 512),
+ SPINAND_INFO_OP_VARIANTS(&read_cache_variants,
+ &write_cache_variants,
+ &update_cache_variants),
+ SPINAND_HAS_QE_BIT,
+ SPINAND_ECCINFO(&fm25s01b_ooblayout,
+ fm25s01b_ecc_get_status)),
+};
+
+static const struct spinand_manufacturer_ops fudan_spinand_manuf_ops = {
+};
+
+const struct spinand_manufacturer fudan_spinand_manufacturer = {
+ .id = SPINAND_MFR_FUDAN,
+ .name = "FUDAN Micron",
+ .chips = fudan_spinand_table,
+ .nchips = ARRAY_SIZE(fudan_spinand_table),
+ .ops = &fudan_spinand_manuf_ops,
+};
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -264,6 +264,7 @@ struct spinand_manufacturer {
extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer;
extern const struct spinand_manufacturer etron_spinand_manufacturer;
extern const struct spinand_manufacturer fidelix_spinand_manufacturer;
+extern const struct spinand_manufacturer fudan_spinand_manufacturer;
extern const struct spinand_manufacturer gigadevice_spinand_manufacturer;
extern const struct spinand_manufacturer macronix_spinand_manufacturer;
extern const struct spinand_manufacturer micron_spinand_manufacturer;

View File

@@ -174,6 +174,12 @@ func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) {
return return
} }
// It seems that mihomo does not implement a connection error reporting mechanism, so we report success directly.
err = stream.HandshakeSuccess()
if err != nil {
return
}
h.NewConnection(ctx, stream, M.Metadata{ h.NewConnection(ctx, stream, M.Metadata{
Source: M.SocksaddrFromNet(conn.RemoteAddr()), Source: M.SocksaddrFromNet(conn.RemoteAddr()),
Destination: destination, Destination: destination,

View File

@@ -15,10 +15,10 @@ import (
const CheckMark = -1 const CheckMark = -1
var DefaultPaddingScheme = []byte(`stop=8 var DefaultPaddingScheme = []byte(`stop=8
0=34-120 0=30-30
1=100-400 1=100-400
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=500-1000 3=9-9,500-1000
4=500-1000 4=500-1000
5=500-1000 5=500-1000
6=500-1000 6=500-1000

View File

@@ -83,11 +83,7 @@ func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) {
} }
stream.dieHook = func() { stream.dieHook = func() {
if session.IsClosed() { if !session.IsClosed() {
if session.dieHook != nil {
session.dieHook()
}
} else {
select { select {
case <-c.die.Done(): case <-c.die.Done():
// Now client has been closed // Now client has been closed
@@ -154,10 +150,10 @@ func (c *Client) Close() error {
c.sessionsLock.Lock() c.sessionsLock.Lock()
sessionToClose := make([]*Session, 0, len(c.sessions)) sessionToClose := make([]*Session, 0, len(c.sessions))
for seq, session := range c.sessions { for _, session := range c.sessions {
sessionToClose = append(sessionToClose, session) sessionToClose = append(sessionToClose, session)
delete(c.sessions, seq)
} }
c.sessions = make(map[uint64]*Session)
c.sessionsLock.Unlock() c.sessionsLock.Unlock()
for _, session := range sessionToClose { for _, session := range sessionToClose {

View File

@@ -9,9 +9,14 @@ const ( // cmds
cmdSYN = 1 // stream open cmdSYN = 1 // stream open
cmdPSH = 2 // data push cmdPSH = 2 // data push
cmdFIN = 3 // stream close, a.k.a EOF mark cmdFIN = 3 // stream close, a.k.a EOF mark
cmdSettings = 4 // Settings cmdSettings = 4 // Settings (Client send to Server)
cmdAlert = 5 // Alert cmdAlert = 5 // Alert
cmdUpdatePaddingScheme = 6 // update padding scheme cmdUpdatePaddingScheme = 6 // update padding scheme
// Since version 2
cmdSYNACK = 7 // Server reports to the client that the stream has been opened
cmdHeartRequest = 8 // Keep alive command
cmdHeartResponse = 9 // Keep alive command
cmdServerSettings = 10 // Settings (Server send to client)
) )
const ( const (

View File

@@ -3,9 +3,11 @@ package session
import ( import (
"crypto/md5" "crypto/md5"
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"net" "net"
"runtime/debug" "runtime/debug"
"strconv"
"sync" "sync"
"time" "time"
@@ -30,11 +32,16 @@ type Session struct {
die chan struct{} die chan struct{}
dieHook func() dieHook func()
synDone func()
synDoneLock sync.Mutex
// pool // pool
seq uint64 seq uint64
idleSince time.Time idleSince time.Time
padding *atomic.TypedValue[*padding.PaddingFactory] padding *atomic.TypedValue[*padding.PaddingFactory]
peerVersion byte
// client // client
isClient bool isClient bool
sendPadding bool sendPadding bool
@@ -76,7 +83,7 @@ func (s *Session) Run() {
} }
settings := util.StringMap{ settings := util.StringMap{
"v": "1", "v": "2",
"client": "mihomo/" + constant.Version, "client": "mihomo/" + constant.Version,
"padding-md5": s.padding.Load().Md5, "padding-md5": s.padding.Load().Md5,
} }
@@ -105,15 +112,16 @@ func (s *Session) Close() error {
close(s.die) close(s.die)
once = true once = true
}) })
if once { if once {
if s.dieHook != nil { if s.dieHook != nil {
s.dieHook() s.dieHook()
s.dieHook = nil
} }
s.streamLock.Lock() s.streamLock.Lock()
for k := range s.streams { for _, stream := range s.streams {
s.streams[k].sessionClose() stream.Close()
} }
s.streams = make(map[uint32]*Stream)
s.streamLock.Unlock() s.streamLock.Unlock()
return s.conn.Close() return s.conn.Close()
} else { } else {
@@ -132,6 +140,17 @@ func (s *Session) OpenStream() (*Stream, error) {
//logrus.Debugln("stream open", sid, s.streams) //logrus.Debugln("stream open", sid, s.streams)
if sid >= 2 && s.peerVersion >= 2 {
s.synDoneLock.Lock()
if s.synDone != nil {
s.synDone()
}
s.synDone = util.NewDeadlineWatcher(time.Second*3, func() {
s.Close()
})
s.synDoneLock.Unlock()
}
if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil {
return nil, err return nil, err
} }
@@ -195,13 +214,37 @@ func (s *Session) recvLoop() error {
if _, ok := s.streams[sid]; !ok { if _, ok := s.streams[sid]; !ok {
stream := newStream(sid, s) stream := newStream(sid, s)
s.streams[sid] = stream s.streams[sid] = stream
if s.onNewStream != nil { go func() {
go s.onNewStream(stream) if s.onNewStream != nil {
} else { s.onNewStream(stream)
go s.Close() } else {
} stream.Close()
}
}()
} }
s.streamLock.Unlock() s.streamLock.Unlock()
case cmdSYNACK: // should be client only
s.synDoneLock.Lock()
if s.synDone != nil {
s.synDone()
s.synDone = nil
}
s.synDoneLock.Unlock()
if hdr.Length() > 0 {
buffer := pool.Get(int(hdr.Length()))
if _, err := io.ReadFull(s.conn, buffer); err != nil {
pool.Put(buffer)
return err
}
// report error
s.streamLock.RLock()
stream, ok := s.streams[sid]
s.streamLock.RUnlock()
if ok {
stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer)))
}
pool.Put(buffer)
}
case cmdFIN: case cmdFIN:
s.streamLock.RLock() s.streamLock.RLock()
stream, ok := s.streams[sid] stream, ok := s.streams[sid]
@@ -240,6 +283,20 @@ func (s *Session) recvLoop() error {
return err return err
} }
} }
// check client's version
if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 {
s.peerVersion = byte(v)
// send cmdServerSettings
f := newFrame(cmdServerSettings, 0)
f.data = util.StringMap{
"v": "2",
}.ToBytes()
_, err = s.writeFrame(f)
if err != nil {
pool.Put(buffer)
return err
}
}
} }
pool.Put(buffer) pool.Put(buffer)
} }
@@ -265,12 +322,35 @@ func (s *Session) recvLoop() error {
} }
if s.isClient { if s.isClient {
if padding.UpdatePaddingScheme(rawScheme, s.padding) { if padding.UpdatePaddingScheme(rawScheme, s.padding) {
log.Infoln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) log.Debugln("[Update padding succeed] %x\n", md5.Sum(rawScheme))
} else { } else {
log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme)) log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme))
} }
} }
} }
case cmdHeartRequest:
if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil {
return err
}
case cmdHeartResponse:
// Active keepalive checking is not implemented yet
break
case cmdServerSettings:
if hdr.Length() > 0 {
buffer := pool.Get(int(hdr.Length()))
if _, err := io.ReadFull(s.conn, buffer); err != nil {
pool.Put(buffer)
return err
}
if s.isClient {
// check server's version
m := util.StringMapFromBytes(buffer)
if v, err := strconv.Atoi(m["v"]); err == nil {
s.peerVersion = byte(v)
}
}
pool.Put(buffer)
}
default: default:
// I don't know what command it is (can't have data) // I don't know what command it is (can't have data)
} }
@@ -280,8 +360,10 @@ func (s *Session) recvLoop() error {
} }
} }
// notify the session that a stream has closed
func (s *Session) streamClosed(sid uint32) error { func (s *Session) streamClosed(sid uint32) error {
if s.IsClosed() {
return io.ErrClosedPipe
}
_, err := s.writeFrame(newFrame(cmdFIN, sid)) _, err := s.writeFrame(newFrame(cmdFIN, sid))
s.streamLock.Lock() s.streamLock.Lock()
delete(s.streams, sid) delete(s.streams, sid)

View File

@@ -22,6 +22,9 @@ type Stream struct {
dieOnce sync.Once dieOnce sync.Once
dieHook func() dieHook func()
dieErr error
reportOnce sync.Once
} }
// newStream initiates a Stream struct // newStream initiates a Stream struct
@@ -36,7 +39,11 @@ func newStream(id uint32, sess *Session) *Stream {
// Read implements net.Conn // Read implements net.Conn
func (s *Stream) Read(b []byte) (n int, err error) { func (s *Stream) Read(b []byte) (n int, err error) {
return s.pipeR.Read(b) n, err = s.pipeR.Read(b)
if s.dieErr != nil {
err = s.dieErr
}
return
} }
// Write implements net.Conn // Write implements net.Conn
@@ -54,25 +61,28 @@ func (s *Stream) Write(b []byte) (n int, err error) {
// Close implements net.Conn // Close implements net.Conn
func (s *Stream) Close() error { func (s *Stream) Close() error {
if s.sessionClose() { return s.CloseWithError(io.ErrClosedPipe)
// notify remote
return s.sess.streamClosed(s.id)
} else {
return io.ErrClosedPipe
}
} }
// sessionClose close stream from session side, do not notify remote func (s *Stream) CloseWithError(err error) error {
func (s *Stream) sessionClose() (once bool) { // if err != io.ErrClosedPipe {
// logrus.Debugln(err)
// }
var once bool
s.dieOnce.Do(func() { s.dieOnce.Do(func() {
s.dieErr = err
s.pipeR.Close() s.pipeR.Close()
once = true once = true
})
if once {
if s.dieHook != nil { if s.dieHook != nil {
s.dieHook() s.dieHook()
s.dieHook = nil s.dieHook = nil
} }
}) return s.sess.streamClosed(s.id)
return } else {
return s.dieErr
}
} }
func (s *Stream) SetReadDeadline(t time.Time) error { func (s *Stream) SetReadDeadline(t time.Time) error {
@@ -108,3 +118,33 @@ func (s *Stream) RemoteAddr() net.Addr {
} }
return nil return nil
} }
// HandshakeFailure should be called when Server fail to create outbound proxy
func (s *Stream) HandshakeFailure(err error) error {
var once bool
s.reportOnce.Do(func() {
once = true
})
if once && err != nil && s.sess.peerVersion >= 2 {
f := newFrame(cmdSYNACK, s.id)
f.data = []byte(err.Error())
if _, err := s.sess.writeFrame(f); err != nil {
return err
}
}
return nil
}
// HandshakeSuccess should be called when Server success to create outbound proxy
func (s *Stream) HandshakeSuccess() error {
var once bool
s.reportOnce.Do(func() {
once = true
})
if once && s.sess.peerVersion >= 2 {
if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,25 @@
package util
import (
"sync"
"time"
)
func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) {
t := time.NewTimer(ddl)
closeCh := make(chan struct{})
go func() {
defer t.Stop()
select {
case <-closeCh:
case <-t.C:
timeOut()
}
}()
var once sync.Once
return func() {
once.Do(func() {
close(closeCh)
})
}
}

View File

@@ -7,13 +7,13 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=alist PKG_NAME:=alist
PKG_VERSION:=3.43.0 PKG_VERSION:=3.44.0
PKG_WEB_VERSION:=3.43.0 PKG_WEB_VERSION:=3.44.0
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=803e3568d055053cbd345834237308708872cd6e154df1da7c00bc46803e4a8a PKG_HASH:=8511e147e912933ba05c31c28f25c4245cc3529f854b0471ba947e33c09f297d
PKG_LICENSE:=GPL-3.0 PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE PKG_LICENSE_FILE:=LICENSE
@@ -23,7 +23,7 @@ define Download/$(PKG_NAME)-web
FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz
URL_FILE:=dist.tar.gz URL_FILE:=dist.tar.gz
URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/ URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/
HASH:=22d1fcbd10af2257029c65add00a589201fabf9509ea57494a6d088b76a4e8a2 HASH:=3709bec59bbc14f0f9f74193cebbb25a317c978fa3a5ae06a900eb341e1b5ae7
endef endef
PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_DEPENDS:=golang/host

View File

@@ -11,7 +11,7 @@ end
local fs = api.fs local fs = api.fs
local sys = api.sys local sys = api.sys
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_geoview = api.is_finded("geoview") local has_geoview = api.is_finded("geoview")
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")

View File

@@ -2,7 +2,7 @@ local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local datatypes = api.datatypes local datatypes = api.datatypes
local fs = api.fs local fs = api.fs
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_geoview = api.is_finded("geoview") local has_geoview = api.is_finded("geoview")
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")

View File

@@ -4,7 +4,7 @@ local appname = "passwall"
local has_ss = api.is_finded("ss-redir") local has_ss = api.is_finded("ss-redir")
local has_ss_rust = api.is_finded("sslocal") local has_ss_rust = api.is_finded("sslocal")
local has_trojan_plus = api.is_finded("trojan-plus") local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") local has_hysteria2 = api.finded_com("hysteria")
local ss_type = {} local ss_type = {}

View File

@@ -25,7 +25,7 @@ end
local has_ss = api.is_finded("ss-redir") local has_ss = api.is_finded("ss-redir")
local has_ss_rust = api.is_finded("sslocal") local has_ss_rust = api.is_finded("sslocal")
local has_trojan_plus = api.is_finded("trojan-plus") local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") local has_hysteria2 = api.finded_com("hysteria")
local ss_type = {} local ss_type = {}

View File

@@ -1,7 +1,7 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local fs = api.fs local fs = api.fs
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_fw3 = api.is_finded("fw3") local has_fw3 = api.is_finded("fw3")
local has_fw4 = api.is_finded("fw4") local has_fw4 = api.is_finded("fw4")

View File

@@ -1,7 +1,7 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)

View File

@@ -9,7 +9,7 @@ if not arg[1] or not m:get(arg[1]) then
luci.http.redirect(m.redirect) luci.http.redirect(m.redirect)
end end
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local nodes_table = {} local nodes_table = {}

View File

@@ -2,7 +2,7 @@ local m, s = ...
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local singbox_bin = api.finded_com("singbox") local singbox_bin = api.finded_com("sing-box")
local geoview_bin = api.is_finded("geoview") local geoview_bin = api.is_finded("geoview")
if not singbox_bin then if not singbox_bin then

View File

@@ -2,7 +2,7 @@ local m, s = ...
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local singbox_bin = api.finded_com("singbox") local singbox_bin = api.finded_com("sing-box")
if not singbox_bin then if not singbox_bin then
return return

View File

@@ -24,7 +24,7 @@ _M.hysteria = {
} }
} }
_M.singbox = { _M["sing-box"] = {
name = "Sing-Box", name = "Sing-Box",
repo = "SagerNet/sing-box", repo = "SagerNet/sing-box",
get_url = gh_release_url, get_url = gh_release_url,

View File

@@ -142,7 +142,7 @@ local function start()
bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path) bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path)
elseif type == "sing-box" then elseif type == "sing-box" then
config = require(require_dir .. "util_sing-box").gen_config_server(user) config = require(require_dir .. "util_sing-box").gen_config_server(user)
bin = ln_run(api.get_app_path("singbox"), "sing-box", "run -c " .. config_file, log_path) bin = ln_run(api.get_app_path("sing-box"), "sing-box", "run -c " .. config_file, log_path)
elseif type == "Xray" then elseif type == "Xray" then
config = require(require_dir .. "util_xray").gen_config_server(user) config = require(require_dir .. "util_xray").gen_config_server(user)
bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path) bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path)

View File

@@ -7,7 +7,7 @@ local appname = "passwall"
local fs = api.fs local fs = api.fs
local split = api.split local split = api.split
local local_version = api.get_app_version("singbox") local local_version = api.get_app_version("sing-box")
local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0") local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0")
local geosite_all_tag = {} local geosite_all_tag = {}

View File

@@ -201,7 +201,7 @@ geosite:category-games'
config shunt_rules 'AIGC' config shunt_rules 'AIGC'
option remarks 'AIGC' option remarks 'AIGC'
option domain_list 'geosite:category-ai-chat-!cn option domain_list 'geosite:category-ai-!cn
domain:apple-relay.apple.com' domain:apple-relay.apple.com'
config shunt_rules 'Streaming' config shunt_rules 'Streaming'

View File

@@ -24,7 +24,7 @@ uci:revert(appname)
local has_ss = api.is_finded("ss-redir") local has_ss = api.is_finded("ss-redir")
local has_ss_rust = api.is_finded("sslocal") local has_ss_rust = api.is_finded("sslocal")
local has_trojan_plus = api.is_finded("trojan-plus") local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") local has_hysteria2 = api.finded_com("hysteria")
local allowInsecure_default = nil local allowInsecure_default = nil

View File

@@ -20,12 +20,12 @@ jobs:
toolchain: stable toolchain: stable
- target: aarch64-unknown-linux-musl - target: aarch64-unknown-linux-musl
toolchain: stable toolchain: stable
- target: mips-unknown-linux-gnu #- target: mips-unknown-linux-gnu
toolchain: nightly # toolchain: nightly
- target: mipsel-unknown-linux-gnu #- target: mipsel-unknown-linux-gnu
toolchain: nightly # toolchain: nightly
- target: mips64el-unknown-linux-gnuabi64 #- target: mips64el-unknown-linux-gnuabi64
toolchain: nightly # toolchain: nightly
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)

View File

@@ -592,18 +592,18 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.32" version = "4.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.32" version = "4.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -1009,7 +1009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1980,7 +1980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@@ -2725,7 +2725,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3054,7 +3054,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3587,9 +3587,9 @@ dependencies = [
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [ dependencies = [
"lock_api", "lock_api",
] ]
@@ -3728,7 +3728,7 @@ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -4346,7 +4346,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@@ -129,7 +129,7 @@ once_cell = "1.17"
thiserror = "2.0" thiserror = "2.0"
arc-swap = "1.7" arc-swap = "1.7"
spin = { version = "0.9" } spin = { version = "0.10" }
lru_time_cache = "0.11" lru_time_cache = "0.11"
bytes = "1.7" bytes = "1.7"
byte_string = "1.0" byte_string = "1.0"

View File

@@ -58,7 +58,7 @@ byte_string = "1.0"
base64 = "0.22" base64 = "0.22"
url = "2.5" url = "2.5"
once_cell = "1.17" once_cell = "1.17"
spin = { version = "0.9", features = ["std"] } spin = { version = "0.10", features = ["std"] }
pin-project = "1.1" pin-project = "1.1"
bloomfilter = { version = "3.0.0", optional = true } bloomfilter = { version = "3.0.0", optional = true }
thiserror = "2.0" thiserror = "2.0"

View File

@@ -176,6 +176,9 @@ jobs:
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}" PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}" echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB - name: Package DEB
if: matrix.debian != '' if: matrix.debian != ''
run: | run: |
@@ -183,7 +186,7 @@ jobs:
sudo gem install fpm sudo gem install fpm
sudo apt-get install -y debsigs sudo apt-get install -y debsigs
fpm -t deb \ fpm -t deb \
-v "${{ needs.calculate_version.outputs.version }}" \ -v "$PKG_VERSION" \
-p "dist/${PKG_NAME}.deb" \ -p "dist/${PKG_NAME}.deb" \
--architecture ${{ matrix.debian }} \ --architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
@@ -200,7 +203,7 @@ jobs:
set -xeuo pipefail set -xeuo pipefail
sudo gem install fpm sudo gem install fpm
fpm -t rpm \ fpm -t rpm \
-v "${{ needs.calculate_version.outputs.version }}" \ -v "$PKG_VERSION" \
-p "dist/${PKG_NAME}.rpm" \ -p "dist/${PKG_NAME}.rpm" \
--architecture ${{ matrix.rpm }} \ --architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
@@ -219,7 +222,7 @@ jobs:
sudo gem install fpm sudo gem install fpm
sudo apt-get install -y libarchive-tools sudo apt-get install -y libarchive-tools
fpm -t pacman \ fpm -t pacman \
-v "${{ needs.calculate_version.outputs.version }}" \ -v "$PKG_VERSION" \
-p "dist/${PKG_NAME}.pkg.tar.zst" \ -p "dist/${PKG_NAME}.pkg.tar.zst" \
--architecture ${{ matrix.pacman }} \ --architecture ${{ matrix.pacman }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box

View File

@@ -109,6 +109,11 @@ jobs:
if: contains(needs.calculate_version.outputs.version, '-') if: contains(needs.calculate_version.outputs.version, '-')
run: |- run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV" echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB - name: Package DEB
if: matrix.debian != '' if: matrix.debian != ''
run: | run: |
@@ -117,7 +122,7 @@ jobs:
sudo apt-get install -y debsigs sudo apt-get install -y debsigs
fpm -t deb \ fpm -t deb \
--name "${NAME}" \ --name "${NAME}" \
-v "${{ needs.calculate_version.outputs.version }}" \ -v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \ --architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
@@ -135,7 +140,7 @@ jobs:
sudo gem install fpm sudo gem install fpm
fpm -t rpm \ fpm -t rpm \
--name "${NAME}" \ --name "${NAME}" \
-v "${{ needs.calculate_version.outputs.version }}" \ -v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \ --architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box

View File

@@ -1,3 +1,3 @@
VERSION_CODE=488 VERSION_CODE=497
VERSION_NAME=1.11.5 VERSION_NAME=1.11.6
GO_VERSION=go1.24.1 GO_VERSION=go1.24.1

View File

@@ -140,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer stream.Close()
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message) err = transport.WriteMessage(stream, 0, message)
if err != nil { if err != nil {
stream.Close()
return nil, err return nil, err
} }
stream.Close()
return transport.ReadMessage(stream) return transport.ReadMessage(stream)
} }

View File

@@ -2,10 +2,16 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.0-alpha.20 #### 1.12.0-alpha.21
* Fixes and improvements * Fixes and improvements
### 1.11.6
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
#### 1.12.0-alpha.19 #### 1.12.0-alpha.19
* Update gVisor to 20250319.0 * Update gVisor to 20250319.0

View File

@@ -44,10 +44,10 @@ Default padding scheme:
``` ```
stop=8 stop=8
0=34-120 0=30-30
1=100-400 1=100-400
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=500-1000 3=9-9,500-1000
4=500-1000 4=500-1000
5=500-1000 5=500-1000
6=500-1000 6=500-1000

View File

@@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。
``` ```
stop=8 stop=8
0=34-120 0=30-30
1=100-400 1=100-400
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=500-1000 3=9-9,500-1000
4=500-1000 4=500-1000
5=500-1000 5=500-1000
6=500-1000 6=500-1000

View File

@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
go 1.23.1 go 1.23.1
require ( require (
github.com/anytls/sing-anytls v0.0.6 github.com/anytls/sing-anytls v0.0.7
github.com/caddyserver/certmagic v0.21.7 github.com/caddyserver/certmagic v0.21.7
github.com/cloudflare/circl v1.6.0 github.com/cloudflare/circl v1.6.0
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0

View File

@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anytls/sing-anytls v0.0.6 h1:UatIjl/OvzWQGXQ1I2bAIkabL9WtihW0fA7G+DXGBUg= github.com/anytls/sing-anytls v0.0.7 h1:0Q5dHNB2sqkFAWZCyK2vjQ/ckI5Iz3V/Frf3k7mBrGc=
github.com/anytls/sing-anytls v0.0.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/anytls/sing-anytls v0.0.7/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=

View File

@@ -2,8 +2,10 @@ package tailscale
import ( import (
"context" "context"
"crypto/tls"
"fmt" "fmt"
"net" "net"
"net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
@@ -212,6 +214,18 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
t.stack = ipStack t.stack = ipStack
localBackend := t.server.ExportLocalBackend() localBackend := t.server.ExportLocalBackend()
localBackend.SetHTTPTestClient(&http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return t.server.Dialer.DialContext(ctx, network, M.ParseSocksaddr(address))
},
TLSClientConfig: &tls.Config{
RootCAs: adapter.RootPoolFromContext(t.ctx),
},
},
})
perfs := &ipn.MaskedPrefs{ perfs := &ipn.MaskedPrefs{
ExitNodeIPSet: true, ExitNodeIPSet: true,
AdvertiseRoutesSet: true, AdvertiseRoutesSet: true,

View File

@@ -11,7 +11,7 @@ end
local fs = api.fs local fs = api.fs
local sys = api.sys local sys = api.sys
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_geoview = api.is_finded("geoview") local has_geoview = api.is_finded("geoview")
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")

View File

@@ -2,7 +2,7 @@ local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local datatypes = api.datatypes local datatypes = api.datatypes
local fs = api.fs local fs = api.fs
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_geoview = api.is_finded("geoview") local has_geoview = api.is_finded("geoview")
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")

View File

@@ -4,7 +4,7 @@ local appname = "passwall"
local has_ss = api.is_finded("ss-redir") local has_ss = api.is_finded("ss-redir")
local has_ss_rust = api.is_finded("sslocal") local has_ss_rust = api.is_finded("sslocal")
local has_trojan_plus = api.is_finded("trojan-plus") local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") local has_hysteria2 = api.finded_com("hysteria")
local ss_type = {} local ss_type = {}

View File

@@ -25,7 +25,7 @@ end
local has_ss = api.is_finded("ss-redir") local has_ss = api.is_finded("ss-redir")
local has_ss_rust = api.is_finded("sslocal") local has_ss_rust = api.is_finded("sslocal")
local has_trojan_plus = api.is_finded("trojan-plus") local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") local has_hysteria2 = api.finded_com("hysteria")
local ss_type = {} local ss_type = {}

View File

@@ -1,7 +1,7 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local fs = api.fs local fs = api.fs
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_fw3 = api.is_finded("fw3") local has_fw3 = api.is_finded("fw3")
local has_fw4 = api.is_finded("fw4") local has_fw4 = api.is_finded("fw4")

View File

@@ -1,7 +1,7 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)

View File

@@ -9,7 +9,7 @@ if not arg[1] or not m:get(arg[1]) then
luci.http.redirect(m.redirect) luci.http.redirect(m.redirect)
end end
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local nodes_table = {} local nodes_table = {}

View File

@@ -2,7 +2,7 @@ local m, s = ...
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local singbox_bin = api.finded_com("singbox") local singbox_bin = api.finded_com("sing-box")
local geoview_bin = api.is_finded("geoview") local geoview_bin = api.is_finded("geoview")
if not singbox_bin then if not singbox_bin then

View File

@@ -2,7 +2,7 @@ local m, s = ...
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local singbox_bin = api.finded_com("singbox") local singbox_bin = api.finded_com("sing-box")
if not singbox_bin then if not singbox_bin then
return return

View File

@@ -24,7 +24,7 @@ _M.hysteria = {
} }
} }
_M.singbox = { _M["sing-box"] = {
name = "Sing-Box", name = "Sing-Box",
repo = "SagerNet/sing-box", repo = "SagerNet/sing-box",
get_url = gh_release_url, get_url = gh_release_url,

View File

@@ -142,7 +142,7 @@ local function start()
bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path) bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path)
elseif type == "sing-box" then elseif type == "sing-box" then
config = require(require_dir .. "util_sing-box").gen_config_server(user) config = require(require_dir .. "util_sing-box").gen_config_server(user)
bin = ln_run(api.get_app_path("singbox"), "sing-box", "run -c " .. config_file, log_path) bin = ln_run(api.get_app_path("sing-box"), "sing-box", "run -c " .. config_file, log_path)
elseif type == "Xray" then elseif type == "Xray" then
config = require(require_dir .. "util_xray").gen_config_server(user) config = require(require_dir .. "util_xray").gen_config_server(user)
bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path) bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path)

View File

@@ -7,7 +7,7 @@ local appname = "passwall"
local fs = api.fs local fs = api.fs
local split = api.split local split = api.split
local local_version = api.get_app_version("singbox") local local_version = api.get_app_version("sing-box")
local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0") local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0")
local geosite_all_tag = {} local geosite_all_tag = {}

View File

@@ -201,7 +201,7 @@ geosite:category-games'
config shunt_rules 'AIGC' config shunt_rules 'AIGC'
option remarks 'AIGC' option remarks 'AIGC'
option domain_list 'geosite:category-ai-chat-!cn option domain_list 'geosite:category-ai-!cn
domain:apple-relay.apple.com' domain:apple-relay.apple.com'
config shunt_rules 'Streaming' config shunt_rules 'Streaming'

View File

@@ -24,7 +24,7 @@ uci:revert(appname)
local has_ss = api.is_finded("ss-redir") local has_ss = api.is_finded("ss-redir")
local has_ss_rust = api.is_finded("sslocal") local has_ss_rust = api.is_finded("sslocal")
local has_trojan_plus = api.is_finded("trojan-plus") local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("singbox") local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") local has_hysteria2 = api.finded_com("hysteria")
local allowInsecure_default = nil local allowInsecure_default = nil

View File

@@ -144,7 +144,7 @@ s = m:section(NamedSection, sid, "servers")
s.anonymous = true s.anonymous = true
s.addremove = false s.addremove = false
o = s:option(DummyValue, "ssr_url", "SS/SSR/V2RAY/TROJAN URL") o = s:option(DummyValue, "ssr_url", "SS/SSR/V2RAY/TROJAN/HYSTERIA2 URL")
o.rawhtml = true o.rawhtml = true
o.template = "shadowsocksr/ssrurl" o.template = "shadowsocksr/ssrurl"
o.value = sid o.value = sid

View File

@@ -10,7 +10,6 @@ require "luci.util"
require "luci.sys" require "luci.sys"
require "luci.jsonc" require "luci.jsonc"
require "luci.model.ipkg" require "luci.model.ipkg"
local ucursor = require "luci.model.uci".cursor()
-- these global functions are accessed all the time by the event handler -- these global functions are accessed all the time by the event handler
-- so caching them is worth the effort -- so caching them is worth the effort
@@ -76,11 +75,20 @@ local encrypt_methods_ss = {
"camellia-256-cfb", "camellia-256-cfb",
"salsa20", "salsa20",
"chacha20", "chacha20",
"chacha20-ietf" ]] "chacha20-ietf" ]]--
} }
-- 分割字符串 -- 分割字符串
local function split(full, sep) local function split(full, sep)
full = full:gsub("%z", "") -- 这里不是很清楚 有时候结尾带个\0 if full == nil or type(full) ~= "string" then
-- print("Debug: split() received nil or non-string value")
return {}
end
full = full:gsub("%z", ""):gsub("^%s+", ""):gsub("%s+$", "") -- 去除首尾空白字符和\0
if full == "" then
-- print("Debug: split() received empty string after trimming")
return {}
end
sep = sep or "," -- 默认分隔符
local off, result = 1, {} local off, result = 1, {}
while true do while true do
local nStart, nEnd = full:find(sep, off) local nStart, nEnd = full:find(sep, off)
@@ -155,13 +163,33 @@ local function checkTabValue(tab)
end end
return revtab return revtab
end end
-- JSON完整性检查
local function isCompleteJSON(str)
-- 检查JSON格式
if type(str) ~= "string" or str:match("^%s*$") then
return false
end
-- 尝试解析JSON验证完整性
local success, _ = pcall(jsonParse, str)
return success
end
-- 处理数据 -- 处理数据
local function processData(szType, content) local function processData(szType, content)
local result = {type = szType, local_port = 1234, kcp_param = '--nocomp'} local result = {type = szType, local_port = 1234, kcp_param = '--nocomp'}
-- 检查JSON的格式如不完整丢弃
if not isCompleteJSON(content) then
return nil
end
if szType == "hysteria2" then if szType == "hysteria2" then
local url = URL.parse("http://" .. content) local url = URL.parse("http://" .. content)
local params = url.query local params = url.query
-- 调试输出所有参数
-- log("Hysteria2 原始参数:")
-- for k,v in pairs(params) do
-- log(k.."="..v)
-- end
result.alias = url.fragment and UrlDecode(url.fragment) or nil result.alias = url.fragment and UrlDecode(url.fragment) or nil
result.type = hy2_type result.type = hy2_type
result.server = url.host result.server = url.host
@@ -171,12 +199,12 @@ local function processData(szType, content)
result.transport_protocol = params.protocol or "udp" result.transport_protocol = params.protocol or "udp"
end end
result.hy2_auth = url.user result.hy2_auth = url.user
result.uplink_capacity = params.upmbps result.uplink_capacity = params.upmbps or "5"
result.downlink_capacity = params.downmbps result.downlink_capacity = params.downmbps or "20"
if params.obfs and params.obfs-password then if params.obfs then
result.flag_obfs = "1" result.flag_obfs = "1"
result.transport_protocol = params.obfs result.obfs_type = params.obfs
result.transport_protocol = params.obfs-password result.salamander = params["obfs-password"] or params["obfs_password"]
end end
if params.sni then if params.sni then
result.tls = "1" result.tls = "1"
@@ -210,8 +238,13 @@ local function processData(szType, content)
result.alias = "[" .. group .. "] " result.alias = "[" .. group .. "] "
end end
result.alias = result.alias .. base64Decode(params.remarks) result.alias = result.alias .. base64Decode(params.remarks)
elseif szType == 'vmess' then elseif szType == "vmess" then
local info = jsonParse(content) -- 解析正常节点
local success, info = pcall(jsonParse, content)
if not success or type(info) ~= "table" then
return nil
end
-- 处理有效数据
result.type = 'v2ray' result.type = 'v2ray'
result.v2ray_protocol = 'vmess' result.v2ray_protocol = 'vmess'
result.server = info.add result.server = info.add
@@ -312,12 +345,21 @@ local function processData(szType, content)
idx_sp = content:find("#") idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1) alias = content:sub(idx_sp + 1, -1)
end end
local info = content:sub(1, idx_sp - 1) local info = content:sub(1, idx_sp > 0 and idx_sp - 1 or #content)
local hostInfo = split(base64Decode(info), "@") local hostInfo = split(base64Decode(info), "@")
if #hostInfo < 2 then
--log("SS节点格式错误解码后内容:", base64Decode(info))
return nil
end
local host = split(hostInfo[2], ":") local host = split(hostInfo[2], ":")
if #host < 2 then
--log("SS节点主机格式错误:", hostInfo[2])
return nil
end
-- 提取用户信息
local userinfo = base64Decode(hostInfo[1]) local userinfo = base64Decode(hostInfo[1])
local method = userinfo:sub(1, userinfo:find(":") - 1) local method, password = userinfo:match("^([^:]*):(.*)$")
local password = userinfo:sub(userinfo:find(":") + 1, #userinfo) -- 填充结果
result.alias = UrlDecode(alias) result.alias = UrlDecode(alias)
result.type = v2_ss result.type = v2_ss
result.v2ray_protocol = (v2_ss == "v2ray") and "shadowsocks" or nil result.v2ray_protocol = (v2_ss == "v2ray") and "shadowsocks" or nil
@@ -325,39 +367,47 @@ local function processData(szType, content)
result.encrypt_method_ss = method result.encrypt_method_ss = method
result.password = password result.password = password
result.server = host[1] result.server = host[1]
if host[2]:find("/%?") then -- 处理端口和插件
local query = split(host[2], "/%?") local port_part = host[2]
if port_part:find("/%?") then
local query = split(port_part, "/%?")
result.server_port = query[1] result.server_port = query[1]
local params = {} if query[2] then
for _, v in pairs(split(query[2], '&')) do local params = {}
local t = split(v, '=') for _, v in pairs(split(query[2], '&')) do
params[t[1]] = t[2] local t = split(v, '=')
end if #t >= 2 then
if params.plugin then params[t[1]] = t[2]
local plugin_info = UrlDecode(params.plugin) end
local idx_pn = plugin_info:find(";")
if idx_pn then
result.plugin = plugin_info:sub(1, idx_pn - 1)
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
else
result.plugin = plugin_info
result.plugin_opts = ""
end end
-- 部分机场下发的插件名为 simple-obfs这里应该改为 obfs-local if params.plugin then
if result.plugin == "simple-obfs" then local plugin_info = UrlDecode(params.plugin)
result.plugin = "obfs-local" local idx_pn = plugin_info:find(";")
end if idx_pn then
-- 如果插件不為 none確保 enable_plugin 為 1 result.plugin = plugin_info:sub(1, idx_pn - 1)
if result.plugin ~= "none" and result.plugin ~= "" then result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
result.enable_plugin = 1 else
result.plugin = plugin_info
result.plugin_opts = ""
end
-- 部分机场下发的插件名为 simple-obfs这里应该改为 obfs-local
if result.plugin == "simple-obfs" then
result.plugin = "obfs-local"
end
-- 如果插件不为 none确保 enable_plugin 为 1
if result.plugin ~= "none" and result.plugin ~= "" then
result.enable_plugin = 1
end
end end
end end
else else
result.server_port = host[2]:gsub("/","") result.server_port = port_part:gsub("/","")
end end
-- 检查加密方法
if not checkTabValue(encrypt_methods_ss)[method] then if not checkTabValue(encrypt_methods_ss)[method] then
-- 1202 年了还不支持 SS AEAD 的屑机场 -- 1202 年了还不支持 SS AEAD 的屑机场
result.server = nil -- log("不支持的SS加密方法:", method)
result.server = nil
end end
elseif szType == "sip008" then elseif szType == "sip008" then
result.type = v2_ss result.type = v2_ss
@@ -601,7 +651,7 @@ local function processData(szType, content)
end end
-- wget -- wget
local function wget(url) local function wget(url)
local stdout = luci.sys.exec('wget-ssl -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -O- "' .. url .. '"') local stdout = luci.sys.exec('wget-ssl --timeout=20 --tries=3 -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -O- "' .. url .. '"')
return trim(stdout) return trim(stdout)
end end
@@ -814,5 +864,3 @@ if subscribe_url and #subscribe_url > 0 then
end end
end) end)
end end

View File

@@ -6,12 +6,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box PKG_NAME:=sing-box
PKG_VERSION:=1.11.5 PKG_VERSION:=1.11.6
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=ac95287a65ae9297aa0b9f25934ead8468bf7ef3f9045e483659ddc400e758a1 PKG_HASH:=01b697683c2433d93e3a15a362241e5e709c01a8e4cc97c889f6d2c5f9db275e
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE

View File

@@ -21,13 +21,13 @@ define Download/geoip
HASH:=83337c712b04d8c16351cf5a5394eae5cb9cfa257fb4773485945dce65dcea76 HASH:=83337c712b04d8c16351cf5a5394eae5cb9cfa257fb4773485945dce65dcea76
endef endef
GEOSITE_VER:=20250326132209 GEOSITE_VER:=20250327125346
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER) GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/ URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE) FILE:=$(GEOSITE_FILE)
HASH:=1eeb1aa717971c6cabd5fa935a74e2a12c4f264b8641cec340309d85524895a7 HASH:=a9114bff9cb6dcdf553b4cd21e56f589a65b7fb1836084b3c32b0acc6f2814a8
endef endef
GEOSITE_IRAN_VER:=202503240038 GEOSITE_IRAN_VER:=202503240038

View File

@@ -19,7 +19,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 22
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- run: git config --global user.name 'Leask Wong' - run: git config --global user.name 'Leask Wong'
- run: git config --global user.email 'i@leaskh.com' - run: git config --global user.email 'i@leaskh.com'

View File

@@ -1,7 +1,7 @@
{ {
"name": "socratex", "name": "socratex",
"description": "A Secure Web Proxy. Which is fast, secure, and easy to use.", "description": "A Secure Web Proxy. Which is fast, secure, and easy to use.",
"version": "2.0.18", "version": "2.0.20",
"private": false, "private": false,
"homepage": "https://github.com/Leask/socratex", "homepage": "https://github.com/Leask/socratex",
"main": "index.mjs", "main": "index.mjs",

View File

@@ -151,7 +151,7 @@ func NewProcess(tmpl *Template,
} }
return nil, fmt.Errorf("unexpected exiting: check the log for more information") return nil, fmt.Errorf("unexpected exiting: check the log for more information")
} }
if time.Since(startTime) > 15*time.Second { if time.Since(startTime) > 30*time.Second {
return nil, fmt.Errorf("timeout: check the log for more information") return nil, fmt.Errorf("timeout: check the log for more information")
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)

View File

@@ -72,6 +72,7 @@ namespace ServiceLib
public const string LocalAppData = "V2RAYN_LOCAL_APPLICATION_DATA_V2"; public const string LocalAppData = "V2RAYN_LOCAL_APPLICATION_DATA_V2";
public const string V2RayLocalAsset = "V2RAY_LOCATION_ASSET"; public const string V2RayLocalAsset = "V2RAY_LOCATION_ASSET";
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET"; public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000; public const int SpeedTestPageSize = 1000;
public const string LinuxBash = "/bin/bash"; public const string LinuxBash = "/bin/bash";

View File

@@ -24,6 +24,7 @@ namespace ServiceLib.Handler
Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(Global.XrayLocalCert, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
//Copy the bin folder to the storage location (for init) //Copy the bin folder to the storage location (for init)
if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1") if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1")

View File

@@ -148,7 +148,7 @@ dependencies {
// UI Libraries // UI Libraries
implementation(libs.material) implementation(libs.material)
implementation(libs.toastcompat) implementation(libs.toasty)
implementation(libs.editorkit) implementation(libs.editorkit)
implementation(libs.flexbox) implementation(libs.flexbox)

View File

@@ -35,7 +35,6 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission <uses-permission
@@ -213,7 +212,8 @@
android:icon="@drawable/ic_stat_name" android:icon="@drawable/ic_stat_name"
android:label="@string/app_tile_name" android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon"> android:process=":RunSoLibV2RayDaemon"
tools:targetApi="24">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>

View File

@@ -8,7 +8,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat import es.dmoral.toasty.Toasty
import org.json.JSONObject import org.json.JSONObject
import java.io.Serializable import java.io.Serializable
import java.net.URI import java.net.URI
@@ -23,7 +23,7 @@ val Context.v2RayApplication: AngApplication?
* @param message The resource ID of the message to show. * @param message The resource ID of the message to show.
*/ */
fun Context.toast(message: Int) { fun Context.toast(message: Int) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() Toasty.normal(this, message).show()
} }
/** /**
@@ -32,9 +32,46 @@ fun Context.toast(message: Int) {
* @param message The text of the message to show. * @param message The text of the message to show.
*/ */
fun Context.toast(message: CharSequence) { fun Context.toast(message: CharSequence) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() Toasty.normal(this, message).show()
} }
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastSuccess(message: Int) {
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastSuccess(message: CharSequence) {
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastError(message: Int) {
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastError(message: CharSequence) {
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
}
/** /**
* Puts a key-value pair into the JSONObject. * Puts a key-value pair into the JSONObject.
* *

View File

@@ -14,6 +14,8 @@ import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.SpeedtestManager import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil import com.v2ray.ang.util.ZipUtil
@@ -50,9 +52,9 @@ class AboutActivity : BaseActivity() {
binding.layoutBackup.setOnClickListener { binding.layoutBackup.setOnClickListener {
val ret = backupConfiguration(extDir.absolutePath) val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) { if (ret.first) {
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
@@ -72,7 +74,7 @@ class AboutActivity : BaseActivity() {
) )
) )
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
@@ -126,7 +128,7 @@ class AboutActivity : BaseActivity() {
} }
} }
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> { private fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
val dateFormated = SimpleDateFormat( val dateFormated = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss", "yyyy-MM-dd-HH-mm-ss",
Locale.getDefault() Locale.getDefault()
@@ -147,7 +149,7 @@ class AboutActivity : BaseActivity() {
} }
} }
fun restoreConfiguration(zipFile: File): Boolean { private fun restoreConfiguration(zipFile: File): Boolean {
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}" val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) { if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
@@ -185,13 +187,13 @@ class AboutActivity : BaseActivity() {
} }
} }
if (restoreConfiguration(targetFile)) { if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e) Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -11,6 +12,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -21,7 +23,7 @@ import java.io.IOException
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
var logsetsAll: MutableList<String> = mutableListOf() private var logsetsAll: MutableList<String> = mutableListOf()
var logsets: MutableList<String> = mutableListOf() var logsets: MutableList<String> = mutableListOf()
private val adapter by lazy { LogcatRecyclerAdapter(this) } private val adapter by lazy { LogcatRecyclerAdapter(this) }
@@ -62,7 +64,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
logsetsAll = allText.toMutableList() logsetsAll = allText.toMutableList()
logsets = allText.toMutableList() logsets = allText.toMutableList()
adapter.notifyDataSetChanged() refreshData()
binding.refreshLayout.isRefreshing = false binding.refreshLayout.isRefreshing = false
} }
} }
@@ -84,7 +86,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
logsetsAll.clear() logsetsAll.clear()
logsets.clear() logsets.clear()
adapter.notifyDataSetChanged() refreshData()
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
@@ -118,7 +120,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.copy_all -> { R.id.copy_all -> {
Utils.setClipboard(this, logsets.joinToString("\n")) Utils.setClipboard(this, logsets.joinToString("\n"))
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
true true
} }
@@ -138,11 +140,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
logsetsAll.filter { it.contains(key) }.toMutableList() logsetsAll.filter { it.contains(key) }.toMutableList()
} }
adapter?.notifyDataSetChanged() refreshData()
return true return true
} }
override fun onRefresh() { override fun onRefresh() {
getLogcat() getLogcat()
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter.notifyDataSetChanged()
}
} }

View File

@@ -1,6 +1,7 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.ColorStateList import android.content.res.ColorStateList
@@ -31,6 +32,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MigrateManager import com.v2ray.ang.handler.MigrateManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
@@ -204,6 +206,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}) })
} }
@SuppressLint("NotifyDataSetChanged")
private fun setupViewModel() { private fun setupViewModel() {
mainViewModel.updateListAction.observe(this) { index -> mainViewModel.updateListAction.observe(this) { index ->
if (index >= 0) { if (index >= 0) {
@@ -269,7 +272,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.tabGroup.isVisible = true binding.tabGroup.isVisible = true
} }
fun startV2Ray() { private fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) { if (MmkvManager.getSelectServer().isNullOrEmpty()) {
toast(R.string.title_file_chooser) toast(R.string.title_file_chooser)
return return
@@ -277,7 +280,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
V2RayServiceManager.startVService(this) V2RayServiceManager.startVService(this)
} }
fun restartV2Ray() { private fun restartV2Ray() {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this) V2RayServiceManager.stopVService(this)
} }
@@ -503,13 +506,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
countSub > 0 -> initGroupTab() countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure) else -> toastError(R.string.toast_failure)
} }
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
e.printStackTrace() e.printStackTrace()
@@ -613,7 +616,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
toast(getString(R.string.title_update_config_count, count)) toast(getString(R.string.title_update_config_count, count))
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
@@ -629,7 +632,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (ret > 0) if (ret > 0)
toast(getString(R.string.title_export_config_count, ret)) toast(getString(R.string.title_export_config_count, ret))
else else
toast(R.string.toast_failure) toastError(R.string.toast_failure)
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} }
@@ -759,9 +762,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// } // }
// if (mainViewModel.appendCustomConfigServer(server)) { // if (mainViewModel.appendCustomConfigServer(server)) {
// mainViewModel.reloadServerList() // mainViewModel.reloadServerList()
// toast(R.string.toast_success) // toastSuccess(R.string.toast_success)
// } else { // } else {
// toast(R.string.toast_failure) // toastError(R.string.toast_failure)
// } // }
// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex) // //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
// } catch (e: Exception) { // } catch (e: Exception) {

Some files were not shown because too many files have changed in this diff Show More