mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-04-13 08:00:30 +08:00
Compare commits
29 Commits
30f87b8439
...
bc4b59c66b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc4b59c66b | ||
|
|
490f559306 | ||
|
|
6bb2304f52 | ||
|
|
ea190b9bc1 | ||
|
|
5f179d3ea5 | ||
|
|
e6cba388b2 | ||
|
|
291abc0a0d | ||
|
|
ae42750f34 | ||
|
|
7b104df463 | ||
|
|
7aea4af2d0 | ||
|
|
1d053fe636 | ||
|
|
b071154263 | ||
|
|
026d9d30f9 | ||
|
|
3d68e57158 | ||
|
|
8af815ee60 | ||
|
|
fdb57431ba | ||
|
|
5eee22292e | ||
|
|
36027cecea | ||
|
|
727eceb0cf | ||
|
|
3d9507b10c | ||
|
|
9d5d2bb73d | ||
|
|
ccaabb7b1a | ||
|
|
45fd8e6870 | ||
|
|
5947394338 | ||
|
|
5f5ca0fd27 | ||
|
|
bab949e16a | ||
|
|
a0bac512dd | ||
|
|
f9176f3fa0 | ||
|
|
2bbf896584 |
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@ -114,9 +114,7 @@ jobs:
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm add @mihomo-party/sysproxy-win32-${{ matrix.arch }}-msvc
|
||||
run: pnpm install
|
||||
- name: Update Version for Dev Build
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
@ -197,9 +195,13 @@ jobs:
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm add @mihomo-party/sysproxy-win32-${{ matrix.arch }}-msvc
|
||||
pnpm add -D electron@22.3.27
|
||||
# Downgrade packages incompatible with Node.js 16 (Electron 22)
|
||||
pnpm add express@4.21.2 chokidar@3.6.0
|
||||
(Get-Content electron-builder.yml) -replace 'windows', 'win7' | Set-Content electron-builder.yml
|
||||
# Electron 22 requires CJS format
|
||||
(Get-Content package.json) -replace '"type": "module"', '"type": "commonjs"' | Set-Content package.json
|
||||
(Get-Content electron.vite.config.ts) -replace 'plugins: \[externalizeDepsPlugin\(\)\]', "plugins: [externalizeDepsPlugin()],`n build: { rollupOptions: { output: { format: 'cjs' } } }" | Set-Content electron.vite.config.ts
|
||||
- name: Update Version for Dev Build
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
@ -280,7 +282,6 @@ jobs:
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm add @mihomo-party/sysproxy-linux-${{ matrix.arch }}-gnu
|
||||
sed -i "s/productName: Clash Party/productName: clash-party/" electron-builder.yml
|
||||
- name: Update Version for Dev Build
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
@ -354,9 +355,7 @@ jobs:
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm add @mihomo-party/sysproxy-darwin-${{ matrix.arch }}
|
||||
run: pnpm install
|
||||
- name: Update Version for Dev Build
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
@ -454,7 +453,6 @@ jobs:
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm add @mihomo-party/sysproxy-darwin-${{ matrix.arch }}
|
||||
pnpm add -D electron@32.2.2
|
||||
- name: Update Version for Dev Build
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
|
||||
45
changelog.md
45
changelog.md
@ -2,6 +2,7 @@
|
||||
|
||||
## 新功能 (Feat)
|
||||
|
||||
- 支持隐藏不可用代理选项
|
||||
- 支持禁用自动更新
|
||||
- 支持交换任务栏点击行为
|
||||
- 支持订阅导入时自动选择直连或代理
|
||||
@ -12,48 +13,30 @@
|
||||
- 托盘代理组样式支持子菜单模式
|
||||
- 增加繁体中文(台湾)翻译
|
||||
- 增加 HTML 检测和配置文件解析错误处理
|
||||
- 将 sysproxy 迁移至 sysproxy-rs
|
||||
|
||||
## 修复 (Fix)
|
||||
|
||||
- 修复 Windows 旧 mihomo 进程导致的 EBUSY 错误
|
||||
- 修复侧边栏卡片水平偏移
|
||||
- macOS DNS 设置使用 helper 服务避免权限问题
|
||||
- 修复首次启动时资源文件复制失败导致程序无法运行的问题
|
||||
- 改进 macOS 助手在重启后套接字丢失时的恢复能力
|
||||
- 使用原子更新修复 changeCurrentProfile
|
||||
- 确保启用 diffWorkDir 时当前配置文件 ID 一致
|
||||
- 修复配置写入队列并防止 IPC 监听器累积
|
||||
- 解决事件监听器内存泄漏并添加错误日志
|
||||
- 修复 RPM 包中的 .build-id 文件冲突
|
||||
- 修复 WebSocket 重连延迟和事件监听器清理
|
||||
- 优化连接页面性能和状态管理
|
||||
- 处理获取 Mihomo 标签时的非数组响应
|
||||
- 确保所有默认配置字段都存在于 config.yaml 中
|
||||
- 处理失败状态码或无效配置文件的订阅
|
||||
- 防止查找配置文件项时的空访问错误
|
||||
- 修复连接详情和日志无法选择的问题
|
||||
- 改进应用实例锁处理
|
||||
- 修复 mixed-port 配置问题
|
||||
- 备份前添加文件存在性检查
|
||||
- 空端口输入处理为 0
|
||||
- 修复覆盖页面中缺失的占位符和错误处理
|
||||
- 修复内核自重启时的竞态条件
|
||||
- 修复端口值为 NaN 时的配置读写问题
|
||||
- 修复 Smart Core 代理组名称替换精度问题
|
||||
- 修复 profile/override 配置中 items 数组未定义导致的错误
|
||||
- 修复 lite 模式下 geo 文件同步到 profile 工作目录
|
||||
- 修复 Linux GNOME 桌面图标和启动器可见性问题
|
||||
- 修复管理员重启时等待新进程启动
|
||||
|
||||
## 优化 (Optimize)
|
||||
|
||||
- 使用通知系统替换 alert() 弹窗
|
||||
- 使用记忆化和状态管理优化连接和日志组件
|
||||
- 跳过 PowerShell 配置文件加载以提升性能
|
||||
|
||||
## 重构 (Refactor)
|
||||
|
||||
- 使用 IPC 通道白名单改进预加载安全性
|
||||
- 简化主进程 IPC 处理器注册
|
||||
- 使用通用调用包装器简化 IPC 层
|
||||
- 移除硬编码的中文字符串并改进国际化覆盖
|
||||
- 移除不再使用的 IPC 代码
|
||||
- 添加缺失的 await 关键字并重构重复代码
|
||||
|
||||
### 其他 (chore)
|
||||
|
||||
- 升级所有依赖项
|
||||
- 升级 GitHub Actions 到最新版本
|
||||
- 确保 ESLint 通过并格式化代码
|
||||
- 优化连接页面性能
|
||||
|
||||
# 1.8.9
|
||||
|
||||
|
||||
@ -56,6 +56,8 @@ pkg:
|
||||
alignment: bottomleft
|
||||
file: build/background.png
|
||||
linux:
|
||||
executableName: mihomo-party
|
||||
icon: build/icon.png
|
||||
desktop:
|
||||
entry:
|
||||
Name: Clash Party
|
||||
@ -63,7 +65,8 @@ linux:
|
||||
Comment: A GUI client based on Mihomo
|
||||
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
|
||||
Keywords: proxy;clash;mihomo;vpn;
|
||||
StartupWMClass: clash-party
|
||||
StartupWMClass: mihomo-party
|
||||
Icon: mihomo-party
|
||||
target:
|
||||
- deb
|
||||
- rpm
|
||||
|
||||
@ -20,7 +20,15 @@ export default defineConfig({
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: 'cjs',
|
||||
entryFileNames: '[name].cjs'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
build: {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "mihomo-party",
|
||||
"version": "1.9.0",
|
||||
"description": "Clash Party",
|
||||
"type": "module",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "mihomo-party-org",
|
||||
"homepage": "https://mihomo.party",
|
||||
|
||||
@ -316,11 +316,68 @@ const resolveEnableLoopback = () =>
|
||||
file: 'enableLoopback.exe',
|
||||
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`
|
||||
})
|
||||
const resolveSysproxy = () =>
|
||||
resolveResource({
|
||||
file: 'sysproxy.exe',
|
||||
downloadURL: `https://github.com/mihomo-party-org/sysproxy/releases/download/${arch}/sysproxy.exe`
|
||||
})
|
||||
/* ======= sysproxy-rs ======= */
|
||||
const SYSPROXY_RS_VERSION = 'v0.1.0'
|
||||
const SYSPROXY_RS_URL_PREFIX = `https://github.com/mihomo-party-org/sysproxy-rs-opti/releases/download/${SYSPROXY_RS_VERSION}`
|
||||
|
||||
function getSysproxyNodeName() {
|
||||
const isMusl = (() => {
|
||||
if (platform !== 'linux') return false
|
||||
try {
|
||||
// 通过 ldd --version 输出判断是否为 musl
|
||||
const output = execSync('ldd --version 2>&1 || true').toString()
|
||||
return output.includes('musl')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
if (arch === 'x64') return 'sysproxy.win32-x64-msvc.node'
|
||||
if (arch === 'arm64') return 'sysproxy.win32-arm64-msvc.node'
|
||||
if (arch === 'ia32') return 'sysproxy.win32-ia32-msvc.node'
|
||||
break
|
||||
case 'darwin':
|
||||
if (arch === 'x64') return 'sysproxy.darwin-x64.node'
|
||||
if (arch === 'arm64') return 'sysproxy.darwin-arm64.node'
|
||||
break
|
||||
case 'linux':
|
||||
if (isMusl) {
|
||||
if (arch === 'x64') return 'sysproxy.linux-x64-musl.node'
|
||||
if (arch === 'arm64') return 'sysproxy.linux-arm64-musl.node'
|
||||
} else {
|
||||
if (arch === 'x64') return 'sysproxy.linux-x64-gnu.node'
|
||||
if (arch === 'arm64') return 'sysproxy.linux-arm64-gnu.node'
|
||||
}
|
||||
break
|
||||
}
|
||||
throw new Error(`Unsupported platform for sysproxy-rs: ${platform}-${arch}`)
|
||||
}
|
||||
|
||||
const resolveSysproxy = async () => {
|
||||
const nodeName = getSysproxyNodeName()
|
||||
const sidecarDir = path.join(cwd, 'extra', 'sidecar')
|
||||
const targetPath = path.join(sidecarDir, nodeName)
|
||||
|
||||
fs.mkdirSync(sidecarDir, { recursive: true })
|
||||
|
||||
// 清理其他平台的 .node 文件
|
||||
const files = fs.readdirSync(sidecarDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.node') && file !== nodeName) {
|
||||
fs.rmSync(path.join(sidecarDir, file))
|
||||
console.log(`[INFO]: removed ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(targetPath)) {
|
||||
fs.rmSync(targetPath)
|
||||
}
|
||||
|
||||
await downloadFile(`${SYSPROXY_RS_URL_PREFIX}/${nodeName}`, targetPath)
|
||||
console.log(`[INFO]: ${nodeName} finished`)
|
||||
}
|
||||
|
||||
const resolveMonitor = async () => {
|
||||
const tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
|
||||
@ -429,8 +486,7 @@ const tasks = [
|
||||
{
|
||||
name: 'sysproxy',
|
||||
func: resolveSysproxy,
|
||||
retry: 5,
|
||||
winOnly: true
|
||||
retry: 5
|
||||
},
|
||||
{
|
||||
name: 'monitor',
|
||||
|
||||
@ -33,6 +33,14 @@ export async function getControledMihomoConfig(force = false): Promise<Partial<I
|
||||
|
||||
// 确保配置包含所有必要的默认字段,处理升级场景
|
||||
controledMihomoConfig = deepMerge(defaultControledMihomoConfig, controledMihomoConfig)
|
||||
|
||||
// 清理端口字段中的 NaN 值,恢复为默认值
|
||||
const portFields = ['mixed-port', 'socks-port', 'port', 'redir-port', 'tproxy-port'] as const
|
||||
for (const field of portFields) {
|
||||
if (typeof controledMihomoConfig[field] !== 'number' || Number.isNaN(controledMihomoConfig[field])) {
|
||||
controledMihomoConfig[field] = defaultControledMihomoConfig[field]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof controledMihomoConfig !== 'object')
|
||||
controledMihomoConfig = defaultControledMihomoConfig
|
||||
@ -43,6 +51,14 @@ export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>):
|
||||
controledMihomoWriteQueue = controledMihomoWriteQueue.then(async () => {
|
||||
const { controlDns = true, controlSniff = true } = await getAppConfig()
|
||||
|
||||
// 过滤端口字段中的 NaN 值,防止写入无效配置
|
||||
const portFields = ['mixed-port', 'socks-port', 'port', 'redir-port', 'tproxy-port'] as const
|
||||
for (const field of portFields) {
|
||||
if (field in patch && (typeof patch[field] !== 'number' || Number.isNaN(patch[field]))) {
|
||||
delete patch[field]
|
||||
}
|
||||
}
|
||||
|
||||
if (patch.hosts) {
|
||||
controledMihomoConfig.hosts = patch.hosts
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export async function getOverrideConfig(force = false): Promise<IOverrideConfig>
|
||||
overrideConfig = parse(data) || { items: [] }
|
||||
}
|
||||
if (typeof overrideConfig !== 'object') overrideConfig = { items: [] }
|
||||
if (!Array.isArray(overrideConfig.items)) overrideConfig.items = []
|
||||
return overrideConfig
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
||||
profileConfig = parse(data) || { items: [] }
|
||||
}
|
||||
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
|
||||
if (!Array.isArray(profileConfig.items)) profileConfig.items = []
|
||||
return structuredClone(profileConfig)
|
||||
}
|
||||
|
||||
@ -46,6 +47,7 @@ export async function updateProfileConfig(
|
||||
const data = await readFile(profileConfigPath(), 'utf-8')
|
||||
profileConfig = parse(data) || { items: [] }
|
||||
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
|
||||
if (!Array.isArray(profileConfig.items)) profileConfig.items = []
|
||||
profileConfig = await updater(structuredClone(profileConfig))
|
||||
result = profileConfig
|
||||
await writeFile(profileConfigPath(), stringify(profileConfig), 'utf-8')
|
||||
|
||||
@ -118,18 +118,42 @@ function main(config) {
|
||||
}
|
||||
|
||||
// 更新规则中的代理组引用
|
||||
// 规则参数列表,这些不是策略组名称
|
||||
const ruleParamsSet = new Set(['no-resolve', 'force-remote-dns', 'prefer-ipv6'])
|
||||
|
||||
if (config.rules && Array.isArray(config.rules)) {
|
||||
config.rules = config.rules.map(rule => {
|
||||
if (typeof rule === 'string') {
|
||||
let updatedRule = rule
|
||||
nameMapping.forEach((newName, oldName) => {
|
||||
// 使用简单的字符串替换,检查是否完全匹配
|
||||
if (updatedRule.includes(oldName)) {
|
||||
updatedRule = updatedRule.split(oldName).join(newName)
|
||||
console.log('[Smart Override] Updated rule reference:', oldName, '→', newName)
|
||||
// 按逗号分割规则,精确匹配策略组名称位置
|
||||
const parts = rule.split(',').map(part => part.trim())
|
||||
|
||||
if (parts.length >= 2) {
|
||||
// 找到策略组名称的位置
|
||||
let targetIndex = -1
|
||||
|
||||
// MATCH 规则:MATCH,策略组
|
||||
if (parts[0] === 'MATCH' && parts.length === 2) {
|
||||
targetIndex = 1
|
||||
} else if (parts.length >= 3) {
|
||||
// 其他规则:TYPE,MATCHER,策略组[,参数...]
|
||||
// 策略组通常在第 3 个位置(索引 2),但需要跳过参数
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
if (!ruleParamsSet.has(parts[i])) {
|
||||
targetIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return updatedRule
|
||||
|
||||
// 只替换策略组名称位置
|
||||
if (targetIndex !== -1 && nameMapping.has(parts[targetIndex])) {
|
||||
const oldName = parts[targetIndex]
|
||||
parts[targetIndex] = nameMapping.get(oldName)
|
||||
console.log('[Smart Override] Updated rule reference:', oldName, '→', nameMapping.get(oldName))
|
||||
return parts.join(',')
|
||||
}
|
||||
}
|
||||
return rule
|
||||
} else if (typeof rule === 'object' && rule !== null) {
|
||||
// 处理对象格式的规则
|
||||
['target', 'proxy'].forEach(field => {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { net } from 'electron'
|
||||
import axios from 'axios'
|
||||
import { getAppConfig, patchAppConfig } from '../config'
|
||||
|
||||
const execPromise = promisify(exec)
|
||||
const helperSocketPath = '/tmp/mihomo-party-helper.sock'
|
||||
|
||||
let setPublicDNSTimer: NodeJS.Timeout | null = null
|
||||
let recoverDNSTimer: NodeJS.Timeout | null = null
|
||||
@ -41,10 +43,18 @@ async function getOriginDNS(): Promise<void> {
|
||||
|
||||
async function setDNS(dns: string): Promise<void> {
|
||||
const service = await getDefaultService()
|
||||
// networksetup 需要 root 权限,通过 osascript 请求管理员权限执行
|
||||
const shell = `networksetup -setdnsservers "${service}" ${dns}`
|
||||
const command = `do shell script "${shell}" with administrator privileges`
|
||||
await execPromise(`osascript -e '${command}'`)
|
||||
try {
|
||||
await axios.post(
|
||||
'http://localhost/dns',
|
||||
{ service, dns },
|
||||
{ socketPath: helperSocketPath }
|
||||
)
|
||||
} catch {
|
||||
// fallback to osascript if helper not available
|
||||
const shell = `networksetup -setdnsservers "${service}" ${dns}`
|
||||
const command = `do shell script "${shell}" with administrator privileges`
|
||||
await execPromise(`osascript -e '${command}'`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPublicDNS(): Promise<void> {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { copyFile, mkdir, writeFile, readFile } from 'fs/promises'
|
||||
import { copyFile, mkdir, writeFile, readFile, stat } from 'fs/promises'
|
||||
import vm from 'vm'
|
||||
import { existsSync, writeFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
@ -160,10 +160,23 @@ async function prepareProfileWorkDir(current: string | undefined): Promise<void>
|
||||
if (!existsSync(mihomoProfileWorkDir(current))) {
|
||||
await mkdir(mihomoProfileWorkDir(current), { recursive: true })
|
||||
}
|
||||
|
||||
const isSourceNewer = async (sourcePath: string, targetPath: string): Promise<boolean> => {
|
||||
try {
|
||||
const [sourceStats, targetStats] = await Promise.all([stat(sourcePath), stat(targetPath)])
|
||||
return sourceStats.mtime > targetStats.mtime
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const copy = async (file: string): Promise<void> => {
|
||||
const targetPath = path.join(mihomoProfileWorkDir(current), file)
|
||||
const sourcePath = path.join(mihomoWorkDir(), file)
|
||||
if (!existsSync(targetPath) && existsSync(sourcePath)) {
|
||||
if (!existsSync(sourcePath)) return
|
||||
// 复制条件:目标不存在 或 源文件更新
|
||||
const shouldCopy = !existsSync(targetPath) || (await isSourceNewer(sourcePath, targetPath))
|
||||
if (shouldCopy) {
|
||||
await copyFile(sourcePath, targetPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +85,8 @@ export function initCoreWatcher(): void {
|
||||
|
||||
coreWatcher = chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {})
|
||||
coreWatcher.on('unlinkDir', async () => {
|
||||
// 等待核心自我更新完成,避免与核心自动重启产生竞态
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||
try {
|
||||
await stopCore(true)
|
||||
await startCore()
|
||||
|
||||
@ -105,14 +105,14 @@ export const mihomoGroups = async (): Promise<IMihomoMixedGroup[]> => {
|
||||
if (proxies.proxies[name] && 'all' in proxies.proxies[name] && !proxies.proxies[name].hidden) {
|
||||
const newGroup = proxies.proxies[name]
|
||||
newGroup.testUrl = url
|
||||
const newAll = newGroup.all.map((name) => proxies.proxies[name])
|
||||
const newAll = (newGroup.all || []).map((name) => proxies.proxies[name])
|
||||
groups.push({ ...newGroup, all: newAll })
|
||||
}
|
||||
})
|
||||
if (!groups.find((group) => group.name === 'GLOBAL')) {
|
||||
const newGlobal = proxies.proxies['GLOBAL'] as IMihomoGroup
|
||||
if (!newGlobal.hidden) {
|
||||
const newAll = newGlobal.all.map((name) => proxies.proxies[name])
|
||||
const newAll = (newGlobal.all || []).map((name) => proxies.proxies[name])
|
||||
groups.push({ ...newGlobal, all: newAll })
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,31 +260,29 @@ export async function restartAsAdmin(forTun: boolean = true): Promise<void> {
|
||||
const args = process.argv.slice(1)
|
||||
const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args
|
||||
|
||||
try {
|
||||
const escapedExePath = exePath.replace(/'/g, "''")
|
||||
const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '")
|
||||
const escapedExePath = exePath.replace(/'/g, "''")
|
||||
const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '")
|
||||
|
||||
const command =
|
||||
restartArgs.length > 0
|
||||
? `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs"`
|
||||
: `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
||||
const command =
|
||||
restartArgs.length > 0
|
||||
? `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs -Wait:$false; exit 0"`
|
||||
: `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs -Wait:$false; exit 0"`
|
||||
|
||||
managerLogger.info('Restarting as administrator with command', command)
|
||||
managerLogger.info('Restarting as administrator with command', command)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
|
||||
if (error) {
|
||||
managerLogger.error('PowerShell execution error', error)
|
||||
managerLogger.error('stderr', stderr)
|
||||
} else {
|
||||
managerLogger.info('PowerShell command executed successfully')
|
||||
reject(new Error(`Failed to restart as administrator: ${error.message}`))
|
||||
return
|
||||
}
|
||||
managerLogger.info('PowerShell command executed successfully, quitting app')
|
||||
setTimeout(() => app.quit(), 500)
|
||||
resolve()
|
||||
})
|
||||
|
||||
app.quit()
|
||||
} catch (error) {
|
||||
managerLogger.error('Failed to restart as administrator', error)
|
||||
throw new Error(`Failed to restart as administrator: ${error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function requestTunPermissions(): Promise<void> {
|
||||
|
||||
@ -13,7 +13,7 @@ async function updateProfile(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function initProfileUpdater(): Promise<void> {
|
||||
const { items, current } = await getProfileConfig()
|
||||
const { items = [], current } = await getProfileConfig()
|
||||
const currentItem = await getCurrentProfileItem()
|
||||
|
||||
for (const item of items.filter((i) => i.id !== current)) {
|
||||
|
||||
@ -39,7 +39,7 @@ async function createFloatingWindow(): Promise<void> {
|
||||
closable: safeMode,
|
||||
backgroundColor: safeMode ? '#ffffff' : useCompatMode ? '#f0f0f0' : '#00000000',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
preload: join(__dirname, '../preload/index.cjs'),
|
||||
spellcheck: false,
|
||||
sandbox: false,
|
||||
nodeIntegration: false,
|
||||
|
||||
@ -23,7 +23,7 @@ async function listGists(token: string): Promise<GistInfo[]> {
|
||||
},
|
||||
responseType: 'json'
|
||||
})
|
||||
return res.data as GistInfo[]
|
||||
return Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
|
||||
async function createGist(token: string, content: string): Promise<void> {
|
||||
|
||||
@ -528,8 +528,10 @@ export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: bo
|
||||
const status = calculateTrayIconStatus(sysProxyEnabled, tunEnabled)
|
||||
const iconPaths = getIconPaths()
|
||||
|
||||
getAppConfig().then(({ disableTrayIconColor = false }) => {
|
||||
getAppConfig().then(({ disableTrayIconColor = false, showTraffic = false }) => {
|
||||
if (!tray) return
|
||||
// macOS 开启流量显示时,由 trayIconUpdate 负责图标更新
|
||||
if (process.platform === 'darwin' && showTraffic) return
|
||||
const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status]
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
@ -549,7 +551,9 @@ export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: bo
|
||||
export async function updateTrayIcon(): Promise<void> {
|
||||
if (!tray) return
|
||||
|
||||
const { disableTrayIconColor = false } = await getAppConfig()
|
||||
const { disableTrayIconColor = false, showTraffic = false } = await getAppConfig()
|
||||
// macOS 开启流量显示时,由 trayIconUpdate 负责图标更新
|
||||
if (process.platform === 'darwin' && showTraffic) return
|
||||
const status = await getTrayIconStatus()
|
||||
const iconPaths = getIconPaths()
|
||||
const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status]
|
||||
|
||||
@ -25,6 +25,25 @@ export interface Response<T = unknown> {
|
||||
url: string
|
||||
}
|
||||
|
||||
// 复用单个 session 用于代理请求
|
||||
let proxySession: Electron.Session | null = null
|
||||
let currentProxyUrl: string | null = null
|
||||
let proxySetupPromise: Promise<void> | null = null
|
||||
|
||||
async function getProxySession(proxyUrl: string): Promise<Electron.Session> {
|
||||
if (!proxySession) {
|
||||
proxySession = session.fromPartition('proxy-requests', { cache: false })
|
||||
}
|
||||
if (currentProxyUrl !== proxyUrl) {
|
||||
proxySetupPromise = proxySession.setProxy({ proxyRules: proxyUrl })
|
||||
currentProxyUrl = proxyUrl
|
||||
}
|
||||
if (proxySetupPromise) {
|
||||
await proxySetupPromise
|
||||
}
|
||||
return proxySession
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request using Chromium's network stack (via electron.net)
|
||||
* This provides better compatibility, HTTP/2 support, and system certificate integration
|
||||
@ -45,27 +64,17 @@ export async function request<T = unknown>(
|
||||
} = options
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let sessionToUse: Electron.Session | undefined = session.defaultSession
|
||||
let tempPartition: string | null = null
|
||||
let sessionToUse: Electron.Session = session.defaultSession
|
||||
|
||||
// Set up proxy if specified
|
||||
const setupProxy = async (): Promise<void> => {
|
||||
if (proxy) {
|
||||
// Create temporary session partition to avoid affecting global proxy settings
|
||||
tempPartition = `temp-request-${Date.now()}-${Math.random()}`
|
||||
sessionToUse = session.fromPartition(tempPartition, { cache: false })
|
||||
const proxyUrl = `${proxy.protocol}://${proxy.host}:${proxy.port}`
|
||||
await sessionToUse.setProxy({ proxyRules: proxyUrl })
|
||||
sessionToUse = await getProxySession(proxyUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = (): void => {
|
||||
// Cleanup temporary session if created
|
||||
if (tempPartition) {
|
||||
// Note: Electron doesn't provide session.destroy(), but temporary sessions
|
||||
// will be garbage collected when no longer referenced
|
||||
sessionToUse = undefined
|
||||
}
|
||||
}
|
||||
|
||||
setupProxy()
|
||||
|
||||
@ -178,16 +178,22 @@ async function initFiles(): Promise<void> {
|
||||
const sourcePath = path.join(resourcesFilesDir(), file)
|
||||
if (!existsSync(sourcePath)) return
|
||||
|
||||
const targets = [
|
||||
path.join(mihomoWorkDir(), file),
|
||||
path.join(mihomoTestDir(), file)
|
||||
]
|
||||
const targets = [path.join(mihomoWorkDir(), file), path.join(mihomoTestDir(), file)]
|
||||
|
||||
await Promise.all(
|
||||
targets.map(async (targetPath) => {
|
||||
const shouldCopy = !existsSync(targetPath) || (await isSourceNewer(sourcePath, targetPath))
|
||||
if (shouldCopy) {
|
||||
if (!shouldCopy) return
|
||||
|
||||
try {
|
||||
await cp(sourcePath, targetPath, { recursive: true, force: true })
|
||||
} catch (error: unknown) {
|
||||
const code = (error as NodeJS.ErrnoException).code
|
||||
if (code === 'EPERM' || code === 'EBUSY') {
|
||||
await initLogger.warn(`Skipping ${file}: file is in use`)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@ -39,7 +39,7 @@ export async function createWindow(): Promise<void> {
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux' ? { icon: icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
preload: join(__dirname, '../preload/index.cjs'),
|
||||
spellcheck: false,
|
||||
sandbox: false,
|
||||
devTools: true
|
||||
|
||||
4
src/native/sysproxy/index.d.ts
vendored
4
src/native/sysproxy/index.d.ts
vendored
@ -22,3 +22,7 @@ export function triggerAutoProxy(enable: boolean, url: string): void
|
||||
export function getSystemProxy(): SysproxyInfo
|
||||
|
||||
export function getAutoProxy(): AutoproxyInfo
|
||||
|
||||
export function setSystemProxy(proxy: SysproxyInfo): void
|
||||
|
||||
export function setAutoProxy(proxy: AutoproxyInfo): void
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
const { join, dirname } = require('path')
|
||||
|
||||
const { platform, arch } = process
|
||||
|
||||
@ -24,6 +24,7 @@ function getBindingName() {
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
if (arch === 'x64') return 'sysproxy.win32-x64-msvc.node'
|
||||
if (arch === 'ia32') return 'sysproxy.win32-ia32-msvc.node'
|
||||
if (arch === 'arm64') return 'sysproxy.win32-arm64-msvc.node'
|
||||
break
|
||||
case 'darwin':
|
||||
@ -43,13 +44,45 @@ function getBindingName() {
|
||||
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
||||
}
|
||||
|
||||
function getResourcesPath() {
|
||||
// 开发环境:优先使用 process.cwd()
|
||||
const cwd = process.cwd()
|
||||
if (existsSync(join(cwd, 'extra', 'sidecar'))) {
|
||||
return cwd
|
||||
}
|
||||
// Electron 打包后的路径
|
||||
if (process.resourcesPath && existsSync(join(process.resourcesPath, 'sidecar'))) {
|
||||
return process.resourcesPath
|
||||
}
|
||||
// 备选:使用 app.getAppPath() (Electron 特有)
|
||||
try {
|
||||
const { app } = require('electron')
|
||||
const appPath = app.getAppPath()
|
||||
if (existsSync(join(appPath, 'extra', 'sidecar'))) {
|
||||
return appPath
|
||||
}
|
||||
} catch {}
|
||||
// 备选:从 __dirname 向上查找
|
||||
let currentDir = __dirname
|
||||
while (currentDir !== dirname(currentDir)) {
|
||||
if (existsSync(join(currentDir, 'extra', 'sidecar'))) {
|
||||
return currentDir
|
||||
}
|
||||
currentDir = dirname(currentDir)
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
function loadBinding() {
|
||||
const bindingName = getBindingName()
|
||||
const resourcesPath = getResourcesPath()
|
||||
|
||||
// 查找项目根目录的 sidecar
|
||||
let currentDir = __dirname
|
||||
while (currentDir !== require('path').dirname(currentDir)) {
|
||||
const sidecarPath = join(currentDir, 'sidecar', bindingName)
|
||||
const searchPaths = [
|
||||
join(resourcesPath, 'sidecar', bindingName),
|
||||
join(resourcesPath, 'extra', 'sidecar', bindingName)
|
||||
]
|
||||
|
||||
for (const sidecarPath of searchPaths) {
|
||||
if (existsSync(sidecarPath)) {
|
||||
try {
|
||||
nativeBinding = require(sidecarPath)
|
||||
@ -58,7 +91,6 @@ function loadBinding() {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
currentDir = require('path').dirname(currentDir)
|
||||
}
|
||||
|
||||
if (loadError) {
|
||||
@ -73,3 +105,5 @@ module.exports.triggerManualProxy = binding.triggerManualProxy
|
||||
module.exports.triggerAutoProxy = binding.triggerAutoProxy
|
||||
module.exports.getSystemProxy = binding.getSystemProxy
|
||||
module.exports.getAutoProxy = binding.getAutoProxy
|
||||
module.exports.setSystemProxy = binding.setSystemProxy
|
||||
module.exports.setAutoProxy = binding.setAutoProxy
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "sysproxy-rs",
|
||||
"version": "0.4.0",
|
||||
"description": "System proxy library for Node.js",
|
||||
"type": "commonjs",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"license": "MIT"
|
||||
|
||||
@ -60,6 +60,8 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
const currentDownloadRef = useRef<number | undefined>(undefined)
|
||||
const hasShowTrafficRef = useRef(false)
|
||||
const drawingRef = useRef(false)
|
||||
// 保存待绘制的流量数据,避免跳过更新导致图标闪烁
|
||||
const pendingTrafficRef = useRef<{ up: number; down: number } | null>(null)
|
||||
|
||||
// Chart.js 配置
|
||||
const chartData = useMemo(() => {
|
||||
@ -138,21 +140,30 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
data.push(info.up + info.down)
|
||||
return data
|
||||
})
|
||||
if (platform === 'darwin' && showTraffic) {
|
||||
if (drawingRef.current) return
|
||||
drawingRef.current = true
|
||||
try {
|
||||
await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef)
|
||||
hasShowTrafficRef.current = true
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
drawingRef.current = false
|
||||
if (platform === 'darwin') {
|
||||
if (showTraffic) {
|
||||
// 保存最新流量数据,确保绘制完成后使用最新值
|
||||
pendingTrafficRef.current = { up: info.up, down: info.down }
|
||||
if (drawingRef.current) return
|
||||
drawingRef.current = true
|
||||
try {
|
||||
// 循环处理待绘制数据,直到没有新数据
|
||||
while (pendingTrafficRef.current) {
|
||||
const { up, down } = pendingTrafficRef.current
|
||||
pendingTrafficRef.current = null
|
||||
await drawSvg(up, down, currentUploadRef, currentDownloadRef)
|
||||
}
|
||||
hasShowTrafficRef.current = true
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
drawingRef.current = false
|
||||
}
|
||||
} else if (hasShowTrafficRef.current) {
|
||||
// 只在从 showTraffic=true 切换到 false 时恢复一次原始图标
|
||||
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
||||
hasShowTrafficRef.current = false
|
||||
}
|
||||
} else {
|
||||
if (!hasShowTrafficRef.current) return
|
||||
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
||||
hasShowTrafficRef.current = false
|
||||
}
|
||||
},
|
||||
[showTraffic]
|
||||
|
||||
@ -3,7 +3,7 @@ import { toast } from '@renderer/components/base/toast'
|
||||
import BorderSwitch from '@renderer/components/base/border-swtich'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { triggerSysProxy, updateTrayIcon, updateTrayIconImmediate } from '@renderer/utils/ipc'
|
||||
import { triggerSysProxy, updateTrayIconImmediate } from '@renderer/utils/ipc'
|
||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||
import { AiOutlineGlobal } from 'react-icons/ai'
|
||||
import React from 'react'
|
||||
@ -50,7 +50,6 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
|
||||
|
||||
window.electron.ipcRenderer.send('updateFloatingWindow')
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
await updateTrayIcon()
|
||||
} catch (e) {
|
||||
await patchAppConfig({ sysProxy: { enable: previousState } })
|
||||
// 回滚图标
|
||||
|
||||
@ -3,7 +3,7 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
||||
import BorderSwitch from '@renderer/components/base/border-swtich'
|
||||
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { restartCore, updateTrayIcon, updateTrayIconImmediate } from '@renderer/utils/ipc'
|
||||
import { restartCore, updateTrayIconImmediate } from '@renderer/utils/ipc'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import React from 'react'
|
||||
@ -99,7 +99,6 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
||||
await restartCore()
|
||||
window.electron.ipcRenderer.send('updateFloatingWindow')
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
await updateTrayIcon()
|
||||
}
|
||||
|
||||
if (iconOnly) {
|
||||
|
||||
@ -130,11 +130,11 @@ const Mihomo: React.FC = () => {
|
||||
const { 'store-selected': storeSelected, 'store-fake-ip': storeFakeIp } = profile
|
||||
|
||||
const [isManualPortChange, setIsManualPortChange] = useState(false)
|
||||
const [mixedPortInput, setMixedPortInput] = useState(showMixedPort || mixedPort)
|
||||
const [socksPortInput, setSocksPortInput] = useState(showSocksPort || socksPort)
|
||||
const [httpPortInput, setHttpPortInput] = useState(showHttpPort || httpPort)
|
||||
const [redirPortInput, setRedirPortInput] = useState(showRedirPort || redirPort)
|
||||
const [tproxyPortInput, setTproxyPortInput] = useState(showTproxyPort || tproxyPort)
|
||||
const [mixedPortInput, setMixedPortInput] = useState(showMixedPort ?? mixedPort)
|
||||
const [socksPortInput, setSocksPortInput] = useState(showSocksPort ?? socksPort)
|
||||
const [httpPortInput, setHttpPortInput] = useState(showHttpPort ?? httpPort)
|
||||
const [redirPortInput, setRedirPortInput] = useState(showRedirPort ?? redirPort)
|
||||
const [tproxyPortInput, setTproxyPortInput] = useState(showTproxyPort ?? tproxyPort)
|
||||
const [externalControllerInput, setExternalControllerInput] = useState(externalController)
|
||||
const [secretInput, setSecretInput] = useState(secret)
|
||||
const [lanAllowedIpsInput, setLanAllowedIpsInput] = useState(lanAllowedIps)
|
||||
@ -702,7 +702,7 @@ const Mihomo: React.FC = () => {
|
||||
size="sm"
|
||||
type="number"
|
||||
className="w-[100px]"
|
||||
value={showMixedPort?.toString()}
|
||||
value={(showMixedPort ?? mixedPort ?? '').toString()}
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
@ -763,7 +763,7 @@ const Mihomo: React.FC = () => {
|
||||
size="sm"
|
||||
type="number"
|
||||
className="w-[100px]"
|
||||
value={showSocksPort?.toString()}
|
||||
value={(showSocksPort ?? socksPort ?? '').toString()}
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
@ -796,7 +796,7 @@ const Mihomo: React.FC = () => {
|
||||
onValueChange={(value) => {
|
||||
patchAppConfig({ enableSocksPort: value })
|
||||
if (value) {
|
||||
const port = appConfig?.showSocksPort || socksPort
|
||||
const port = appConfig?.showSocksPort ?? socksPort
|
||||
onChangeNeedRestart({ 'socks-port': port })
|
||||
} else {
|
||||
onChangeNeedRestart({ 'socks-port': 0 })
|
||||
@ -824,7 +824,7 @@ const Mihomo: React.FC = () => {
|
||||
size="sm"
|
||||
type="number"
|
||||
className="w-[100px]"
|
||||
value={showHttpPort?.toString()}
|
||||
value={(showHttpPort ?? httpPort ?? '').toString()}
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
@ -857,7 +857,7 @@ const Mihomo: React.FC = () => {
|
||||
onValueChange={(value) => {
|
||||
patchAppConfig({ enableHttpPort: value })
|
||||
if (value) {
|
||||
const port = appConfig?.showHttpPort || httpPort
|
||||
const port = appConfig?.showHttpPort ?? httpPort
|
||||
onChangeNeedRestart({ port: port })
|
||||
} else {
|
||||
onChangeNeedRestart({ port: 0 })
|
||||
@ -886,7 +886,7 @@ const Mihomo: React.FC = () => {
|
||||
size="sm"
|
||||
type="number"
|
||||
className="w-[100px]"
|
||||
value={showRedirPort?.toString()}
|
||||
value={(showRedirPort ?? redirPort ?? '').toString()}
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
@ -919,7 +919,7 @@ const Mihomo: React.FC = () => {
|
||||
onValueChange={(value) => {
|
||||
patchAppConfig({ enableRedirPort: value })
|
||||
if (value) {
|
||||
const port = appConfig?.showRedirPort || redirPort
|
||||
const port = appConfig?.showRedirPort ?? redirPort
|
||||
onChangeNeedRestart({ 'redir-port': port })
|
||||
} else {
|
||||
onChangeNeedRestart({ 'redir-port': 0 })
|
||||
@ -949,7 +949,7 @@ const Mihomo: React.FC = () => {
|
||||
size="sm"
|
||||
type="number"
|
||||
className="w-[100px]"
|
||||
value={showTproxyPort?.toString()}
|
||||
value={(showTproxyPort ?? tproxyPort ?? '').toString()}
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
@ -982,7 +982,7 @@ const Mihomo: React.FC = () => {
|
||||
onValueChange={(value) => {
|
||||
patchAppConfig({ enableTproxyPort: value })
|
||||
if (value) {
|
||||
const port = appConfig?.showTproxyPort || tproxyPort
|
||||
const port = appConfig?.showTproxyPort ?? tproxyPort
|
||||
onChangeNeedRestart({ 'tproxy-port': port })
|
||||
} else {
|
||||
onChangeNeedRestart({ 'tproxy-port': 0 })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user