diff --git a/electron/preload/index.ts b/electron/preload/index.ts
index 36c68f9..6b8c523 100644
--- a/electron/preload/index.ts
+++ b/electron/preload/index.ts
@@ -87,7 +87,15 @@ contextBridge.exposeInMainWorld('singbox', {
/** 重启内核 */
restart: (config: string) => ipcRenderer.invoke('singbox-restart', config),
/** 确保核心已安装 */
- ensureCoreInstalled: () => ipcRenderer.invoke('singbox-ensure-core'),
+ ensureCoreInstalled: (preferredVersion?: string) => ipcRenderer.invoke('singbox-ensure-core', preferredVersion),
+ /** 安装或重装指定版本核心 */
+ installCore: (preferredVersion?: string) => ipcRenderer.invoke('singbox-install-core', preferredVersion),
+ /** 获取可用核心版本列表 */
+ listCoreVersions: (forceRefresh?: boolean) => ipcRenderer.invoke('singbox-list-core-versions', forceRefresh),
+ /** 获取当前首选核心版本 */
+ getPreferredVersion: () => ipcRenderer.invoke('singbox-get-preferred-version'),
+ /** 设置首选核心版本 */
+ setPreferredVersion: (preferredVersion?: string) => ipcRenderer.invoke('singbox-set-preferred-version', preferredVersion),
/** 获取核心安装状态 */
getInstallInfo: () => ipcRenderer.invoke('singbox-get-install-info'),
})
diff --git a/electron/services/singbox/config.ts b/electron/services/singbox/config.ts
index f2c0733..a9f8cfe 100644
--- a/electron/services/singbox/config.ts
+++ b/electron/services/singbox/config.ts
@@ -92,7 +92,15 @@ export class SingBoxConfigManager {
hints.push('提示:看起来是权限不足导致启动失败,请尝试“以管理员身份运行”。')
}
- if (text.includes('invalid') || text.includes('unknown field') || text.includes('parse') || text.includes('json')) {
+ if (
+ text.includes('invalid')
+ || text.includes('unknown field')
+ || text.includes('parse')
+ || text.includes('json')
+ || text.includes('deprecated')
+ || text.includes('removed in sing-box')
+ || text.includes('use rule actions instead')
+ ) {
hints.push('提示:节点/配置参数可能不兼容当前 sing-box 版本,请把弹窗里的最后一行报错发我。')
}
diff --git a/electron/services/singbox/index.ts b/electron/services/singbox/index.ts
index f73986f..cf68d1d 100644
--- a/electron/services/singbox/index.ts
+++ b/electron/services/singbox/index.ts
@@ -65,15 +65,32 @@ export class SingBoxService {
await this.start(configPath)
})
})
- ipcMain.handle('singbox-ensure-core', async () => {
+ ipcMain.handle('singbox-ensure-core', async (_, preferredVersion?: string) => {
+ if (preferredVersion) {
+ this.installer.setPreferredVersion(preferredVersion)
+ }
await this.installer.checkAndDownloadBinary()
})
+ ipcMain.handle('singbox-install-core', async (_, preferredVersion?: string) => {
+ this.installer.setPreferredVersion(preferredVersion)
+ if (this.process) {
+ throw new Error('sing-box 正在运行,请先停止加速后再切换核心版本')
+ }
+ await this.installer.installCore(preferredVersion)
+ })
+ ipcMain.handle('singbox-list-core-versions', async (_, forceRefresh?: boolean) => {
+ return this.installer.listAvailableVersions(Boolean(forceRefresh))
+ })
+ ipcMain.handle('singbox-get-preferred-version', async () => {
+ return this.installer.getPreferredVersion()
+ })
+ ipcMain.handle('singbox-set-preferred-version', async (_, preferredVersion?: string) => {
+ return this.installer.setPreferredVersion(preferredVersion)
+ })
ipcMain.handle('singbox-get-install-info', async () => {
- const exists = await this.installer.checkBinaryExists()
return {
- exists,
- installDir: this.installer.getInstallDir(),
- binaryPath: this.installer.getBinaryPath()
+ ...await this.installer.getInstallInfo(),
+ isRunning: !!this.process
}
})
}
diff --git a/electron/services/singbox/installer.ts b/electron/services/singbox/installer.ts
index 81cf3cf..af65b6c 100644
--- a/electron/services/singbox/installer.ts
+++ b/electron/services/singbox/installer.ts
@@ -6,6 +6,9 @@ import { pipeline } from 'stream/promises'
import { createWriteStream } from 'fs'
import { Readable } from 'stream'
import https from 'node:https'
+import { app } from 'electron'
+import pkg from '../../../package.json'
+import { JsonStore } from '../../common/store'
import { generateId } from '../../utils/id'
import { BIN_DIR } from './constants'
@@ -36,7 +39,46 @@ export interface InstallerStatus {
isTimeout?: boolean
}
-export type StatusFn = (status: InstallerStatus) => void
+export interface SingBoxCoreRelease {
+ version: string
+ tagName: string
+ publishedAt?: string
+ channel: 'stable' | 'beta' | 'alpha'
+}
+
+export interface SingBoxInstallInfo {
+ exists: boolean
+ installDir: string
+ binaryPath: string
+ installedVersion?: string
+ preferredVersion: string
+}
+
+interface ReleaseAssetResolution {
+ version: string
+ tagName: string
+ downloadUrl: string
+ archiveExt: string
+}
+
+interface InstallMeta {
+ version: string
+ tagName?: string
+ downloadUrl?: string
+ installedAt: number
+}
+
+type StatusFn = (status: InstallerStatus) => void
+
+const GITHUB_RELEASES_BASE = 'https://api.github.com/repos/SagerNet/sing-box/releases'
+const RELEASE_CACHE_TTL_MS = 5 * 60 * 1000
+const TESTED_SINGBOX_VERSION = normalizePackageVersion(pkg.lagZeroTestSingboxVersion)
+
+function normalizePackageVersion(version: unknown) {
+ const raw = String(version || '').trim()
+ if (!raw || raw.toLowerCase() === 'latest') return ''
+ return raw.replace(/^v/i, '')
+}
/**
* Sing-box 安装器
@@ -46,10 +88,19 @@ export class SingBoxInstaller {
private log: LogFn
private onStatus?: StatusFn
private installingPromise: Promise | null = null
+ private settingsStore: JsonStore<{ preferredVersion: string }>
+ private releaseCache: { fetchedAt: number, versions: SingBoxCoreRelease[] } | null = null
constructor(logFn: LogFn, statusFn?: StatusFn) {
this.log = logFn
this.onStatus = statusFn
+ this.settingsStore = new JsonStore({
+ name: 'singbox-core-settings',
+ cwd: app.getPath('userData'),
+ defaults: {
+ preferredVersion: 'latest'
+ }
+ })
}
getInstallDir() {
@@ -61,6 +112,73 @@ export class SingBoxInstaller {
return path.join(BIN_DIR, `sing-box${ext}`)
}
+ getPreferredVersion() {
+ return this.normalizeRequestedVersion(this.settingsStore.get().preferredVersion)
+ }
+
+ setPreferredVersion(version?: string) {
+ const normalized = this.normalizeRequestedVersion(version)
+ const current = this.settingsStore.get()
+ if (current.preferredVersion !== normalized) {
+ this.settingsStore.set({
+ ...current,
+ preferredVersion: normalized
+ })
+ }
+ return normalized
+ }
+
+ async listAvailableVersions(forceRefresh = false): Promise {
+ const now = Date.now()
+ if (!forceRefresh && this.releaseCache && now - this.releaseCache.fetchedAt < RELEASE_CACHE_TTL_MS) {
+ return this.releaseCache.versions
+ }
+
+ let versions: SingBoxCoreRelease[]
+ try {
+ const releases = await this.fetchJson(`${GITHUB_RELEASES_BASE}?per_page=50`)
+ versions = releases
+ .filter((release) => !release?.draft)
+ .reduce((acc, release) => {
+ const tagName = String(release?.tag_name || '').trim()
+ const version = this.normalizeVersionFromTag(tagName)
+ if (!version) return acc
+ acc.push({
+ version,
+ tagName,
+ publishedAt: typeof release?.published_at === 'string' ? release.published_at : undefined,
+ channel: this.inferReleaseChannel(release)
+ })
+ return acc
+ }, [])
+ } catch (error) {
+ if (!TESTED_SINGBOX_VERSION) throw error
+ this.log(`获取 sing-box 版本列表失败,已回退到保底版本 v${TESTED_SINGBOX_VERSION}:${String((error as any)?.message || error)}`, 'error')
+ versions = []
+ }
+
+ versions = this.ensureTestedVersionPresent(versions)
+
+ this.releaseCache = {
+ fetchedAt: now,
+ versions
+ }
+ return versions
+ }
+
+ async getInstallInfo(): Promise {
+ const exists = await this.checkBinaryExists()
+ const meta = exists ? await this.readInstallMeta() : null
+
+ return {
+ exists,
+ installDir: this.getInstallDir(),
+ binaryPath: this.getBinaryPath(),
+ installedVersion: meta?.version,
+ preferredVersion: this.getPreferredVersion()
+ }
+ }
+
async checkBinaryExists() {
const binPath = this.getBinaryPath()
if (await fs.pathExists(binPath)) return true
@@ -68,7 +186,7 @@ export class SingBoxInstaller {
}
/**
- * 检查 sing-box 是否存在,不存在则自动下载
+ * 检查 sing-box 是否存在,不存在则根据首选版本自动下载
* @returns 可执行文件的绝对路径
*/
async checkAndDownloadBinary(): Promise {
@@ -77,7 +195,8 @@ export class SingBoxInstaller {
return this.installingPromise
}
- this.installingPromise = this.checkAndDownloadBinaryInternal()
+ const requestedVersion = this.getPreferredVersion()
+ this.installingPromise = this.checkAndDownloadBinaryInternal(requestedVersion)
try {
return await this.installingPromise
} finally {
@@ -85,7 +204,25 @@ export class SingBoxInstaller {
}
}
- private async checkAndDownloadBinaryInternal(): Promise {
+ /**
+ * 安装指定版本的 sing-box(会覆盖现有核心)
+ */
+ async installCore(version?: string): Promise {
+ if (this.installingPromise) {
+ this.log('检测到已有 sing-box 安装任务,等待完成...')
+ return this.installingPromise
+ }
+
+ const requestedVersion = this.setPreferredVersion(version)
+ this.installingPromise = this.installCoreInternal(requestedVersion)
+ try {
+ return await this.installingPromise
+ } finally {
+ this.installingPromise = null
+ }
+ }
+
+ private async checkAndDownloadBinaryInternal(requestedVersion: string): Promise {
await fs.ensureDir(BIN_DIR)
const platform = process.platform
const arch = process.arch
@@ -93,28 +230,49 @@ export class SingBoxInstaller {
this.emitStatus({ phase: 'checking' })
if (await this.checkBinaryExists()) {
- this.emitStatus({ phase: 'ready' })
+ const meta = await this.readInstallMeta()
+ this.emitStatus({ phase: 'ready', version: meta?.version })
return binPath
}
this.emitStatus({ phase: 'missing' })
- return this.downloadAndInstallBinary(platform, arch, binPath)
+ return this.downloadAndInstallBinary(platform, arch, binPath, requestedVersion)
+ }
+
+ private async installCoreInternal(requestedVersion: string): Promise {
+ await fs.ensureDir(BIN_DIR)
+ const platform = process.platform
+ const arch = process.arch
+ const binPath = this.getBinaryPath()
+ this.emitStatus({ phase: 'checking' })
+ return this.downloadAndInstallBinary(platform, arch, binPath, requestedVersion)
}
/**
* 执行下载和安装流程
*/
- private async downloadAndInstallBinary(platform: NodeJS.Platform, arch: string, binPath: string): Promise {
- this.log('未检测到 sing-box,开始自动下载...')
- this.emitStatus({ phase: 'resolving' })
+ private async downloadAndInstallBinary(
+ platform: NodeJS.Platform,
+ arch: string,
+ binPath: string,
+ requestedVersion: string
+ ): Promise {
+ const displayVersion = requestedVersion === 'latest' ? 'latest' : requestedVersion
+ this.log(`开始准备 sing-box ${displayVersion} 安装包...`)
+ this.emitStatus({
+ phase: 'resolving',
+ version: requestedVersion === 'latest' ? undefined : requestedVersion
+ })
let version: string | undefined
+ let tagName: string | undefined
let downloadUrl: string | undefined
let archivePath = ''
let extractDir = ''
try {
- const resolved = await this.resolveLatestReleaseDownload(platform, arch)
+ const resolved = await this.resolveReleaseDownload(platform, arch, requestedVersion)
version = resolved.version
+ tagName = resolved.tagName
downloadUrl = resolved.downloadUrl
const archiveExt = resolved.archiveExt
this.emitStatus({
@@ -125,7 +283,7 @@ export class SingBoxInstaller {
downloadedBytes: 0
})
- archivePath = path.join(BIN_DIR, `sing-box-${version}${archiveExt}`)
+ archivePath = path.join(BIN_DIR, `sing-box-${version}-${generateId()}${archiveExt}`)
extractDir = path.join(BIN_DIR, `tmp-${generateId()}`)
await fs.ensureDir(extractDir)
await this.downloadToFile(downloadUrl, archivePath, (progress, downloadedBytes, totalBytes) => {
@@ -150,6 +308,12 @@ export class SingBoxInstaller {
throw new Error('安装完成后仍未找到 sing-box 可执行文件')
}
+ await this.writeInstallMeta({
+ version,
+ tagName,
+ downloadUrl,
+ installedAt: Date.now()
+ })
this.log(`sing-box 已安装:${binPath}`)
this.emitStatus({
phase: 'completed',
@@ -171,22 +335,26 @@ export class SingBoxInstaller {
throw new Error(msg)
} finally {
if (extractDir) {
- await fs.remove(extractDir).catch(() => {})
+ await fs.remove(extractDir).catch(() => { })
}
if (archivePath) {
- await fs.remove(archivePath).catch(() => {})
+ await fs.remove(archivePath).catch(() => { })
}
}
}
/**
- * 解析 GitHub 最新 Release 版本信息
+ * 解析 GitHub Release 版本信息
*/
- private async resolveLatestReleaseDownload(platform: NodeJS.Platform, arch: string): Promise<{ version: string, downloadUrl: string, archiveExt: string }> {
+ private async resolveReleaseDownload(
+ platform: NodeJS.Platform,
+ arch: string,
+ requestedVersion: string
+ ): Promise {
const platformName = platform === 'win32' ? 'windows'
: platform === 'darwin' ? 'darwin'
- : platform === 'linux' ? 'linux'
- : null
+ : platform === 'linux' ? 'linux'
+ : null
if (!platformName) {
throw new Error(`不支持的平台:${platform}`)
@@ -194,30 +362,74 @@ export class SingBoxInstaller {
const archName = arch === 'x64' ? 'amd64'
: arch === 'arm64' ? 'arm64'
- : arch === 'ia32' ? '386'
- : null
+ : arch === 'ia32' ? '386'
+ : null
if (!archName) {
throw new Error(`不支持的架构:${arch}`)
}
const archiveExt = platformName === 'windows' ? '.zip' : '.tar.gz'
+ const requestedCandidates = this.buildReleaseRequestCandidates(requestedVersion)
+ let lastError: unknown = null
- const release = await this.fetchJson('https://api.github.com/repos/SagerNet/sing-box/releases/latest')
- const tagName = String(release?.tag_name || '')
- const version = tagName.startsWith('v') ? tagName.slice(1) : tagName
- if (!version) throw new Error('无法解析 sing-box 版本号')
+ for (const candidate of requestedCandidates) {
+ try {
+ const release = candidate === 'latest'
+ ? await this.fetchJson(`${GITHUB_RELEASES_BASE}/latest`)
+ : await this.fetchReleaseByVersion(candidate)
- const assetName = `sing-box-${version}-${platformName}-${archName}${archiveExt}`
- const assets: any[] = Array.isArray(release?.assets) ? release.assets : []
- const asset = assets.find(a => String(a?.name || '') === assetName)
- const downloadUrl = String(asset?.browser_download_url || '')
+ const tagName = String(release?.tag_name || '').trim()
+ const version = this.normalizeVersionFromTag(tagName)
+ if (!version) throw new Error('无法解析 sing-box 版本号')
- if (!downloadUrl) {
- throw new Error(`未找到对应的发布包:${assetName}`)
+ const assetName = `sing-box-${version}-${platformName}-${archName}${archiveExt}`
+ const assets: any[] = Array.isArray(release?.assets) ? release.assets : []
+ const asset = assets.find((a) => String(a?.name || '') === assetName)
+ const downloadUrl = String(asset?.browser_download_url || '')
+
+ if (!downloadUrl) {
+ throw new Error(`未找到对应的发布包:${assetName}`)
+ }
+
+ if (candidate !== requestedCandidates[0]) {
+ this.log(`sing-box latest 不可用,已回退到保底版本 v${version}`)
+ }
+
+ return { version, tagName, downloadUrl, archiveExt }
+ } catch (error) {
+ lastError = error
+ if (candidate !== requestedCandidates.at(-1)) {
+ this.log(`解析 sing-box ${candidate} 失败,尝试保底版本:${String((error as any)?.message || error)}`, 'error')
+ continue
+ }
+ }
}
- return { version, downloadUrl, archiveExt }
+ throw lastError instanceof Error
+ ? lastError
+ : new Error(`未找到可用的 sing-box 发布包:${requestedVersion}`)
+ }
+
+ private async fetchReleaseByVersion(requestedVersion: string): Promise {
+ const candidates = this.buildTagCandidates(requestedVersion)
+ let lastError: unknown = null
+
+ for (const tag of candidates) {
+ try {
+ return await this.fetchJson(`${GITHUB_RELEASES_BASE}/tags/${encodeURIComponent(tag)}`)
+ } catch (error) {
+ lastError = error
+ const message = String((error as any)?.message || error || '')
+ if (!message.includes('404')) {
+ throw error
+ }
+ }
+ }
+
+ throw lastError instanceof Error
+ ? lastError
+ : new Error(`未找到 sing-box 版本:${requestedVersion}`)
}
/**
@@ -274,7 +486,7 @@ export class SingBoxInstaller {
await this.withRetry(
async (attempt) => {
if (attempt > 1) {
- await fs.remove(destPath).catch(() => {})
+ await fs.remove(destPath).catch(() => { })
}
await this.doDownloadRequest(url, destPath, 5, onProgress)
},
@@ -420,8 +632,8 @@ export class SingBoxInstaller {
const exeDir = path.dirname(exePath)
const entries = await fs.readdir(exeDir, { withFileTypes: true })
const dlls = entries
- .filter(e => e.isFile() && e.name.toLowerCase().endsWith('.dll'))
- .map(e => path.join(exeDir, e.name))
+ .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.dll'))
+ .map((e) => path.join(exeDir, e.name))
for (const dll of dlls) {
const target = path.join(BIN_DIR, path.basename(dll))
@@ -429,7 +641,7 @@ export class SingBoxInstaller {
}
}
- await fs.chmod(binPath, 0o755).catch(() => {})
+ await fs.chmod(binPath, 0o755).catch(() => { })
}
/**
@@ -462,7 +674,8 @@ export class SingBoxInstaller {
if (path.resolve(manualPath) === path.resolve(binPath)) return true
await fs.copyFile(manualPath, binPath)
- await fs.chmod(binPath, 0o755).catch(() => {})
+ await fs.chmod(binPath, 0o755).catch(() => { })
+ await this.clearInstallMeta()
if (process.platform === 'win32') {
const manualDir = path.dirname(manualPath)
@@ -470,7 +683,7 @@ export class SingBoxInstaller {
for (const entry of entries) {
if (!entry.isFile()) continue
if (!entry.name.toLowerCase().endsWith('.dll')) continue
- await fs.copyFile(path.join(manualDir, entry.name), path.join(BIN_DIR, entry.name)).catch(() => {})
+ await fs.copyFile(path.join(manualDir, entry.name), path.join(BIN_DIR, entry.name)).catch(() => { })
}
}
@@ -478,6 +691,103 @@ export class SingBoxInstaller {
return true
}
+ private getInstallMetaPath() {
+ return path.join(BIN_DIR, 'sing-box-install-meta.json')
+ }
+
+ private async readInstallMeta(): Promise {
+ try {
+ const meta = await fs.readJson(this.getInstallMetaPath()) as Partial
+ if (!meta || typeof meta !== 'object' || typeof meta.version !== 'string' || !meta.version.trim()) {
+ return null
+ }
+ return {
+ version: meta.version.trim(),
+ tagName: typeof meta.tagName === 'string' ? meta.tagName : undefined,
+ downloadUrl: typeof meta.downloadUrl === 'string' ? meta.downloadUrl : undefined,
+ installedAt: typeof meta.installedAt === 'number' ? meta.installedAt : 0
+ }
+ } catch {
+ return null
+ }
+ }
+
+ private async writeInstallMeta(meta: InstallMeta) {
+ await fs.ensureDir(path.dirname(this.getInstallMetaPath()))
+ await fs.writeJson(this.getInstallMetaPath(), meta, { spaces: 2 })
+ }
+
+ private async clearInstallMeta() {
+ await fs.remove(this.getInstallMetaPath()).catch(() => { })
+ }
+
+ private normalizeRequestedVersion(version?: string) {
+ const raw = String(version || '').trim()
+ if (!raw || raw.toLowerCase() === 'latest') return 'latest'
+ return raw.replace(/^v/i, '')
+ }
+
+ private normalizeVersionFromTag(tagName: string) {
+ return String(tagName || '').trim().replace(/^v/i, '')
+ }
+
+ private inferReleaseChannel(release: any): 'stable' | 'beta' | 'alpha' {
+ const text = `${String(release?.tag_name || '')} ${String(release?.name || '')}`.toLowerCase()
+ if (text.includes('alpha')) return 'alpha'
+ if (
+ text.includes('beta')
+ || /\brc\b/.test(text)
+ || text.includes('preview')
+ || text.includes('pre-release')
+ || release?.prerelease
+ ) {
+ return 'beta'
+ }
+ return 'stable'
+ }
+
+ private buildTagCandidates(version: string) {
+ const normalized = this.normalizeRequestedVersion(version)
+ if (normalized === 'latest') return ['latest']
+ const original = String(version || '').trim()
+ return [...new Set([
+ original,
+ normalized,
+ `v${normalized}`
+ ].filter(Boolean))]
+ }
+
+ private buildReleaseRequestCandidates(version: string) {
+ const normalized = this.normalizeRequestedVersion(version)
+ if (normalized !== 'latest' || !TESTED_SINGBOX_VERSION || TESTED_SINGBOX_VERSION === normalized) {
+ return [normalized]
+ }
+ return ['latest', TESTED_SINGBOX_VERSION]
+ }
+
+ private ensureTestedVersionPresent(versions: SingBoxCoreRelease[]) {
+ if (!TESTED_SINGBOX_VERSION) return versions
+ if (versions.some((release) => release.version === TESTED_SINGBOX_VERSION)) {
+ return versions
+ }
+
+ return [
+ {
+ version: TESTED_SINGBOX_VERSION,
+ tagName: `v${TESTED_SINGBOX_VERSION}`,
+ channel: this.inferReleaseChannelFromVersion(TESTED_SINGBOX_VERSION)
+ },
+ ...versions
+ ]
+ }
+
+ private inferReleaseChannelFromVersion(version: string): 'stable' | 'beta' | 'alpha' {
+ const text = String(version || '').toLowerCase()
+ if (text.includes('alpha')) return 'alpha'
+ if (text.includes('beta') || /\brc\b/.test(text) || text.includes('preview')) return 'beta'
+ return 'stable'
+ }
+
private emitStatus(payload: Omit) {
this.onStatus?.({
...payload,
diff --git a/package.json b/package.json
index ffbcaa9..9ff8d71 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,8 @@
"name": "lagzero",
"productName": "LagZero",
"private": true,
- "version": "0.2.1",
+ "version": "0.2.2",
+ "lagZeroTestSingboxVersion": "1.13.4",
"main": "dist-electron/index.js",
"type": "module",
"author": "LagZero Team",
diff --git a/src/api/singbox.ts b/src/api/singbox.ts
index 19bc6c6..ce1993d 100644
--- a/src/api/singbox.ts
+++ b/src/api/singbox.ts
@@ -2,6 +2,10 @@ export const singboxApi = {
start: (config: string) => window.singbox.start(config),
stop: () => window.singbox.stop(),
restart: (config: string) => window.singbox.restart(config),
- ensureCoreInstalled: () => window.singbox.ensureCoreInstalled(),
+ ensureCoreInstalled: (preferredVersion?: string) => window.singbox.ensureCoreInstalled(preferredVersion),
+ installCore: (preferredVersion?: string) => window.singbox.installCore(preferredVersion),
+ listCoreVersions: (forceRefresh?: boolean) => window.singbox.listCoreVersions(forceRefresh),
+ getPreferredVersion: () => window.singbox.getPreferredVersion(),
+ setPreferredVersion: (preferredVersion?: string) => window.singbox.setPreferredVersion(preferredVersion),
getInstallInfo: () => window.singbox.getInstallInfo(),
}
diff --git a/src/components/settings/NetworkSettings.vue b/src/components/settings/NetworkSettings.vue
index 6718f64..6368abc 100644
--- a/src/components/settings/NetworkSettings.vue
+++ b/src/components/settings/NetworkSettings.vue
@@ -113,6 +113,151 @@
+
+
+
+ {{ $t('settings.core_management') }}
+
+
+
+
+
+
+
{{ $t('settings.core_management') }}
+
+ {{ $t('settings.core_version_desc') }}
+
+
+
+
+
+ {{ coreStatusText }}
+
+
+
+
+
+
+
{{ $t('settings.core_current_version') }}
+
+ {{ installedCoreVersionText }}
+
+
+
+
+
{{ $t('settings.core_test_version') }}
+
+ {{ testedCoreVersionText }}
+
+
+
+
+
{{ $t('settings.core_version') }}
+
+
+
+ {{ $t('settings.core_target_version', { version: selectedCoreVersionText }) }}
+
+
+
+
+
+
{{ $t('settings.core_install_dir') }}
+
+ {{ coreInstallInfo.installDir || '-' }}
+
+
+
+
+ {{ $t('settings.core_version_fetch_failed', { error: coreVersionError }) }}
+
+
+ {{ $t('settings.core_switch_requires_stop') }}
+
+
+
+
+
+
+
+ {{ $t('settings.core_download') }}
+
+
+
+
+
+ {{ $t('settings.core_refresh_versions') }}
+
+
+
+
+
+ {{ $t('settings.core_refresh_status') }}
+
+
+
+
+
+ {{ $t('settings.core_open_dir') }}
+
+
+
+
+
+
+ {{ $t('singbox_installer.download_progress') }}
+
+
+ {{ coreInstallPhaseSummary }}
+
+
+
+
+
+ {{ coreInstallPhaseSummary }}
+
+
+ {{ coreInstallPhaseDetail }}
+
+
+
+
+
+
@@ -387,18 +532,37 @@
diff --git a/src/components/singbox/SingboxInstallerGuard.vue b/src/components/singbox/SingboxInstallerGuard.vue
index 9ea7d60..f322e45 100644
--- a/src/components/singbox/SingboxInstallerGuard.vue
+++ b/src/components/singbox/SingboxInstallerGuard.vue
@@ -1,8 +1,14 @@
-
+
@@ -17,19 +23,51 @@
-
{{ t('singbox_installer.download_url') }}
-
{{ effectiveDownloadUrl }}
+
{{ t('settings.core_version') }}
+
+
+
+ {{ t('settings.core_target_version_label') }}
+ {{ selectedCoreVersionText }}
+
+
+ {{ t('settings.core_test_version') }}
+ {{ testedCoreVersionText }}
+
+
+
+ {{ t('settings.core_version_fetch_failed', { error: versionLoadError }) }}
+
-
- {{ t('singbox_installer.open_download_url') }}
+
+ {{ t('singbox_installer.refresh_versions') }}
-
{{ t('singbox_installer.install_dir') }}
-
{{ installDir }}
-
+
{{ t('singbox_installer.resource_info') }}
+
+
+
{{ t('singbox_installer.download_url') }}
+
{{ effectiveDownloadUrl }}
+
+
+
{{ t('singbox_installer.install_dir') }}
+
{{ installDir }}
+
+
+
+
+ {{ t('singbox_installer.open_download_url') }}
+
{{ t('singbox_installer.open_install_dir') }}
@@ -38,8 +76,13 @@
{{ t('singbox_installer.download_progress') }}
-
+
{{ phaseText }}
@@ -64,8 +107,13 @@
-
- {{ t('singbox_installer.retry_download') }}
+
+ {{ downloadButtonText }}
{{ t('singbox_installer.recheck') }}
@@ -77,9 +125,12 @@
-
+ :bordered="true"
+ >
@@ -89,8 +140,13 @@
-
+
{{ t('singbox_installer.failed') }}
@@ -105,52 +161,95 @@
-
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 47d1809..c331ea0 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -243,6 +243,33 @@
"dns_secondary": "Secondary DNS",
"dns_bootstrap": "Bootstrap DNS",
"dns_bootstrap_hint": "Used to resolve remote DNS domains (e.g. dns.google). Prefer a directly reachable DNS IP such as 223.5.5.5.",
+ "core_management": "sing-box Core",
+ "core_version": "Core Version",
+ "core_version_desc": "Choose core versions by Stable, Beta, or Alpha channels, then lock and install the one you want.",
+ "core_version_latest": "Latest stable (auto)",
+ "core_version_latest_with_version": "Latest stable (auto, current {version})",
+ "core_version_group_stable": "Stable",
+ "core_version_group_beta": "Beta",
+ "core_version_group_alpha": "Alpha",
+ "core_current_version": "Installed Version",
+ "core_test_version": "Tested Core Version",
+ "core_test_version_unset": "Not set",
+ "core_target_version_label": "Download Target",
+ "core_target_version": "Current download target: {version}",
+ "core_install_dir": "Core Install Directory",
+ "core_download": "Download / Reinstall Core",
+ "core_refresh_versions": "Refresh Version List",
+ "core_refresh_status": "Refresh Install Status",
+ "core_open_dir": "Open Core Folder",
+ "core_installed": "Installed",
+ "core_missing": "Not Installed",
+ "core_installed_unknown": "Installed (version unknown, likely manually placed)",
+ "core_switch_requires_stop": "The core is currently running. Stop acceleration before switching core versions.",
+ "core_version_fetch_failed": "Failed to load core versions: {error}",
+ "core_status_load_failed": "Failed to load core install status: {error}",
+ "core_download_success": "Core downloaded successfully: {version}",
+ "core_download_failed": "Core download failed: {error}",
+ "core_open_dir_failed": "Failed to open core folder: {error}",
"download_update": "Download Update Now",
"tagline": "A game accelerator specially designed for enhancing gaming experience",
"preferences": "Preferences",
@@ -381,16 +408,19 @@
"open_download_url": "Open Download URL",
"install_dir": "Core Installation Folder",
"open_install_dir": "Open Core Folder",
+ "resource_info": "Download & Install Info",
"download_progress": "Download Progress",
"download_timeout": "Download timed out, please install manually",
"download_failed": "Download failed, please install manually",
"auto_download_failed": "Automatic download failed. Please download manually and place it in the installation folder.",
"manual_install_guide": "Manual installation: Download and extract the archive, then place `sing-box.exe` (Windows) or `sing-box` (Linux/macOS) into the directory above.",
+ "download_selected": "Download Selected Version",
+ "refresh_versions": "Refresh Versions",
"recheck": "I have placed the core, recheck",
"retry_download": "Retry Download",
"checking": "Checking local core...",
- "missing": "Core not found, preparing to download...",
- "resolving": "Resolving latest version download URL...",
+ "missing": "Core not found. Select a version before downloading.",
+ "resolving": "Resolving core download URL...",
"downloading": "Downloading core...",
"downloading_percent": "Downloading core: {percent}%",
"extracting": "Download complete, installing core...",
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index d281e3f..29d21c1 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -243,6 +243,33 @@
"dns_secondary": "备用 DNS",
"dns_bootstrap": "Bootstrap DNS",
"dns_bootstrap_hint": "用于解析远端 DNS 域名(例如 dns.google)。推荐填写可直连的 DNS IP,如 223.5.5.5。",
+ "core_management": "sing-box 核心",
+ "core_version": "核心版本",
+ "core_version_desc": "支持按正式版、Beta、Alpha 分类选择核心版本,可锁定兼容版本并手动下载/重装。",
+ "core_version_latest": "最新稳定版(自动)",
+ "core_version_latest_with_version": "最新稳定版(自动,当前 {version})",
+ "core_version_group_stable": "正式版",
+ "core_version_group_beta": "Beta 版本",
+ "core_version_group_alpha": "Alpha 版本",
+ "core_current_version": "当前已安装版本",
+ "core_test_version": "测试所用核心版本",
+ "core_test_version_unset": "未设置",
+ "core_target_version_label": "当前下载目标",
+ "core_target_version": "当前下载目标:{version}",
+ "core_install_dir": "核心安装目录",
+ "core_download": "下载/重装核心",
+ "core_refresh_versions": "刷新版本列表",
+ "core_refresh_status": "刷新安装状态",
+ "core_open_dir": "打开核心目录",
+ "core_installed": "已安装",
+ "core_missing": "未安装",
+ "core_installed_unknown": "已安装(版本未知,可能为手动放置)",
+ "core_switch_requires_stop": "检测到核心正在运行,请先停止当前加速后再切换核心版本。",
+ "core_version_fetch_failed": "获取核心版本列表失败:{error}",
+ "core_status_load_failed": "读取核心安装状态失败:{error}",
+ "core_download_success": "核心已下载完成:{version}",
+ "core_download_failed": "核心下载失败:{error}",
+ "core_open_dir_failed": "打开核心目录失败:{error}",
"download_update": "立即下载更新",
"tagline": "一款专为游戏体验打造的游戏加速器",
"preferences": "偏好设置",
@@ -381,16 +408,19 @@
"open_download_url": "打开下载地址",
"install_dir": "核心安装文件夹",
"open_install_dir": "打开核心文件夹",
+ "resource_info": "下载与安装信息",
"download_progress": "下载进度",
"download_timeout": "下载超时,请手动安装核心",
"download_failed": "下载失败,请手动安装核心",
"auto_download_failed": "自动下载失败,请手动下载后放到安装文件夹。",
"manual_install_guide": "手动安装方式:下载压缩包并解压,把 `sing-box.exe`(Windows)或 `sing-box`(Linux/macOS)放到上述目录。",
+ "download_selected": "下载所选版本",
+ "refresh_versions": "刷新版本列表",
"recheck": "我已放置核心,重新检测",
"retry_download": "重试自动下载",
"checking": "正在检测本地核心...",
- "missing": "未检测到核心,准备下载...",
- "resolving": "正在解析最新版本下载地址...",
+ "missing": "未检测到核心,请先选择版本后下载。",
+ "resolving": "正在解析核心下载地址...",
"downloading": "正在下载核心...",
"downloading_percent": "正在下载核心:{percent}%",
"extracting": "下载完成,正在安装核心...",
@@ -419,4 +449,4 @@
"show_window": "显示主界面",
"quit": "退出"
}
-}
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index 35beb61..cb16865 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,7 +10,9 @@ import { initLatencySessionStore } from './utils/latency-session'
import { setupRuntimeLogging } from './utils/runtime-logger'
import { useLocalProxyStore } from './stores/local-proxy'
import { useNodeStore } from './stores/nodes'
+import { useSingboxInstallerStore } from './stores/singbox-installer'
import { useSettingsStore } from './stores/settings'
+import { singboxApi } from './api'
setupRuntimeLogging()
@@ -30,13 +32,38 @@ const isTrayWindow = window.location.hash.includes('/tray')
if (!isTrayWindow) {
const localProxyStore = useLocalProxyStore()
const nodeStore = useNodeStore()
+ const singboxInstallerStore = useSingboxInstallerStore()
const settingsStore = useSettingsStore()
let pendingStartAfterStarting = false
+ const syncPreferredCoreVersion = async (version: string) => {
+ try {
+ const normalized = await singboxApi.setPreferredVersion(version)
+ if (normalized && normalized !== settingsStore.singboxCoreVersion) {
+ settingsStore.singboxCoreVersion = normalized
+ }
+ } catch (e) {
+ console.warn('同步 sing-box 核心版本偏好失败:', e)
+ }
+ }
+
+ void syncPreferredCoreVersion(settingsStore.singboxCoreVersion)
+ void singboxInstallerStore.initialize().catch((e) => {
+ console.warn('初始化 sing-box 安装状态失败:', e)
+ })
+
void localProxyStore.startLocalProxy('startup').catch((e) => {
console.error('启动时启动本地代理失败:', e)
})
+ watch(
+ () => settingsStore.singboxCoreVersion,
+ (version, prev) => {
+ if (version === prev) return
+ void syncPreferredCoreVersion(version)
+ }
+ )
+
watch(
() => nodeStore.nodes.length,
(len) => {
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index b690b7b..6c5f865 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -70,6 +70,7 @@ export const useSettingsStore = defineStore('settings', () => {
'
\nlocalhost\n127.0.0.1\n::1\n192.168.*\n10.*\n*.local'
) // 系统代理绕过列表
const tunInterfaceName = useLocalStorage('settings-tun-interface-name', 'LagZero') // TUN接口名称 默认LagZero
+ const singboxCoreVersion = useLocalStorage('settings-singbox-core-version', 'latest') // sing-box 首选核心版本
if (tunInterfaceName.value === 'singbox-tun') {
tunInterfaceName.value = 'LagZero'
}
@@ -108,6 +109,7 @@ export const useSettingsStore = defineStore('settings', () => {
systemProxyPort,
systemProxyBypass,
tunInterfaceName,
+ singboxCoreVersion,
sessionNetworkTuning,
resetSessionNetworkTuning,
applySessionNetworkProfilePreset
diff --git a/src/stores/singbox-installer.ts b/src/stores/singbox-installer.ts
new file mode 100644
index 0000000..9379f42
--- /dev/null
+++ b/src/stores/singbox-installer.ts
@@ -0,0 +1,287 @@
+import { computed, ref, watch } from 'vue'
+import { defineStore } from 'pinia'
+import { singboxApi } from '@/api'
+import { useSettingsStore } from './settings'
+
+export type SingboxInstallerPhase =
+ | 'idle'
+ | 'checking'
+ | 'ready'
+ | 'missing'
+ | 'resolving'
+ | 'downloading'
+ | 'extracting'
+ | 'completed'
+ | 'failed'
+
+export interface SingboxInstallerStatusEvent {
+ phase: Exclude
+ installDir: string
+ binaryPath: string
+ version?: string
+ downloadUrl?: string
+ progress?: number
+ error?: string
+ isTimeout?: boolean
+}
+
+export interface SingboxCoreReleaseOption {
+ version: string
+ tagName: string
+ publishedAt?: string
+ channel: 'stable' | 'beta' | 'alpha'
+}
+
+export interface SingboxInstallInfo {
+ exists: boolean
+ installDir: string
+ binaryPath: string
+ installedVersion?: string
+ preferredVersion: string
+ isRunning: boolean
+}
+
+const ACTIVE_INSTALL_PHASES = new Set([
+ 'checking',
+ 'resolving',
+ 'downloading',
+ 'extracting'
+])
+
+function createDefaultInstallInfo(preferredVersion = 'latest'): SingboxInstallInfo {
+ return {
+ exists: false,
+ installDir: '',
+ binaryPath: '',
+ installedVersion: undefined,
+ preferredVersion,
+ isRunning: false
+ }
+}
+
+function normalizeProgress(progress?: number) {
+ return typeof progress === 'number'
+ ? Math.max(0, Math.min(100, Math.floor(progress)))
+ : undefined
+}
+
+export const useSingboxInstallerStore = defineStore('singbox-installer', () => {
+ const settingsStore = useSettingsStore()
+
+ const initialized = ref(false)
+ const availableCoreVersions = ref([])
+ const loadingCoreVersions = ref(false)
+ const versionLoadError = ref('')
+ const refreshingInstallInfo = ref(false)
+ const installInfo = ref(createDefaultInstallInfo(settingsStore.singboxCoreVersion))
+ const phase = ref('idle')
+ const progress = ref(undefined)
+ const resolvedVersion = ref('')
+ const downloadUrl = ref('')
+ const errorMessage = ref('')
+ const failedByTimeout = ref(false)
+ const installCallCount = ref(0)
+
+ let initializePromise: Promise | null = null
+ let refreshCoreVersionsPromise: Promise | null = null
+ let refreshInstallInfoPromise: Promise | null = null
+ let installCorePromise: Promise | null = null
+ let statusListenerBound = false
+
+ const isInstalling = computed(() => (
+ installCallCount.value > 0 || ACTIVE_INSTALL_PHASES.has(phase.value)
+ ))
+ const hasInstallProgress = computed(() => (
+ phase.value !== 'idle' && phase.value !== 'ready' && phase.value !== 'completed'
+ ))
+
+ watch(
+ () => settingsStore.singboxCoreVersion,
+ (version, prev) => {
+ if (version === prev) return
+
+ installInfo.value = {
+ ...installInfo.value,
+ preferredVersion: version
+ }
+
+ if (isInstalling.value) return
+
+ downloadUrl.value = ''
+ progress.value = undefined
+ resolvedVersion.value = ''
+ errorMessage.value = ''
+ failedByTimeout.value = false
+ phase.value = installInfo.value.exists ? 'ready' : 'missing'
+ }
+ )
+
+ function applyInstallInfo(next: SingboxInstallInfo) {
+ installInfo.value = next
+ }
+
+ function syncPhaseFromInstallInfo() {
+ if (isInstalling.value) return
+ phase.value = installInfo.value.exists ? 'ready' : 'missing'
+ }
+
+ function applyInstallerStatus(payload: SingboxInstallerStatusEvent) {
+ phase.value = payload.phase
+ resolvedVersion.value = String(payload.version || '').trim()
+ downloadUrl.value = payload.downloadUrl || downloadUrl.value
+ errorMessage.value = String(payload.error || '')
+ failedByTimeout.value = Boolean(payload.isTimeout)
+ progress.value = normalizeProgress(payload.progress)
+
+ installInfo.value = {
+ ...installInfo.value,
+ installDir: payload.installDir || installInfo.value.installDir,
+ binaryPath: payload.binaryPath || installInfo.value.binaryPath
+ }
+
+ if (payload.phase === 'ready' || payload.phase === 'completed') {
+ installInfo.value = {
+ ...installInfo.value,
+ exists: true,
+ installedVersion: resolvedVersion.value || installInfo.value.installedVersion
+ }
+ void refreshInstallInfo()
+ return
+ }
+
+ if (payload.phase === 'missing') {
+ installInfo.value = {
+ ...installInfo.value,
+ exists: false,
+ installedVersion: undefined
+ }
+ }
+ }
+
+ function bindStatusListener() {
+ if (statusListenerBound || typeof window === 'undefined' || typeof window.electron?.on !== 'function') {
+ return
+ }
+
+ window.electron.on('singbox-installer-status', applyInstallerStatus)
+ statusListenerBound = true
+ }
+
+ async function initialize() {
+ if (initialized.value) return
+ if (initializePromise) return initializePromise
+
+ bindStatusListener()
+ initializePromise = (async () => {
+ try {
+ await refreshInstallInfo({ syncPhase: true })
+ } catch (e: any) {
+ phase.value = 'failed'
+ errorMessage.value = String(e?.message || e)
+ throw e
+ } finally {
+ initialized.value = true
+ initializePromise = null
+ }
+ })()
+
+ return initializePromise
+ }
+
+ async function refreshCoreVersions(forceRefresh = false) {
+ if (refreshCoreVersionsPromise) return refreshCoreVersionsPromise
+
+ loadingCoreVersions.value = true
+ versionLoadError.value = ''
+ refreshCoreVersionsPromise = (async () => {
+ try {
+ const versions = await singboxApi.listCoreVersions(forceRefresh)
+ availableCoreVersions.value = versions
+ return versions
+ } catch (e: any) {
+ versionLoadError.value = String(e?.message || e || '')
+ throw e
+ } finally {
+ loadingCoreVersions.value = false
+ refreshCoreVersionsPromise = null
+ }
+ })()
+
+ return refreshCoreVersionsPromise
+ }
+
+ async function refreshInstallInfo(options: { syncPhase?: boolean } = {}) {
+ if (refreshInstallInfoPromise) {
+ const info = await refreshInstallInfoPromise
+ if (options.syncPhase) syncPhaseFromInstallInfo()
+ return info
+ }
+
+ refreshingInstallInfo.value = true
+ refreshInstallInfoPromise = (async () => {
+ try {
+ const info = await singboxApi.getInstallInfo()
+ applyInstallInfo(info)
+ if (options.syncPhase) syncPhaseFromInstallInfo()
+ return info
+ } finally {
+ refreshingInstallInfo.value = false
+ refreshInstallInfoPromise = null
+ }
+ })()
+
+ return refreshInstallInfoPromise
+ }
+
+ async function installCore(preferredVersion?: string) {
+ bindStatusListener()
+ if (installCorePromise) return installCorePromise
+
+ installCallCount.value += 1
+ downloadUrl.value = ''
+ progress.value = undefined
+ resolvedVersion.value = ''
+ errorMessage.value = ''
+ failedByTimeout.value = false
+ phase.value = 'checking'
+
+ installCorePromise = (async () => {
+ try {
+ await singboxApi.installCore(preferredVersion)
+ await refreshInstallInfo()
+ } catch (e: any) {
+ if (phase.value !== 'failed') {
+ phase.value = 'failed'
+ errorMessage.value = String(e?.message || e)
+ }
+ throw e
+ } finally {
+ installCallCount.value = Math.max(0, installCallCount.value - 1)
+ installCorePromise = null
+ }
+ })()
+
+ return installCorePromise
+ }
+
+ return {
+ initialized,
+ availableCoreVersions,
+ loadingCoreVersions,
+ versionLoadError,
+ refreshingInstallInfo,
+ installInfo,
+ phase,
+ progress,
+ resolvedVersion,
+ downloadUrl,
+ errorMessage,
+ failedByTimeout,
+ isInstalling,
+ hasInstallProgress,
+ initialize,
+ refreshCoreVersions,
+ refreshInstallInfo,
+ installCore
+ }
+})
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index 99adaca..2efd240 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -24,11 +24,23 @@ export interface SingboxAPI {
start: (config: string) => Promise
stop: () => Promise
restart: (config: string) => Promise
- ensureCoreInstalled: () => Promise
+ ensureCoreInstalled: (preferredVersion?: string) => Promise
+ installCore: (preferredVersion?: string) => Promise
+ listCoreVersions: (forceRefresh?: boolean) => Promise>
+ getPreferredVersion: () => Promise
+ setPreferredVersion: (preferredVersion?: string) => Promise
getInstallInfo: () => Promise<{
exists: boolean
installDir: string
binaryPath: string
+ installedVersion?: string
+ preferredVersion: string
+ isRunning: boolean
}>
}
diff --git a/src/utils/singbox-config.ts b/src/utils/singbox-config.ts
index 9d21d1d..1fe0a8a 100644
--- a/src/utils/singbox-config.ts
+++ b/src/utils/singbox-config.ts
@@ -152,22 +152,13 @@ export function generateSingboxConfig(game: Game, node: NodeConfig, dnsOptions?:
{
type: 'direct',
tag: 'direct'
- },
- // 阻断出口
- {
- type: 'block',
- tag: 'block'
- },
- // DNS 出口(用于劫持 DNS 流量)
- {
- type: 'dns',
- tag: 'dns-out'
}
],
route: {
rules: [
- // 所有的 DNS 流量都转发给 DNS 模块处理
- { protocol: 'dns', outbound: 'dns-out' }
+ // sing-box 1.13+ 已移除 inbound.sniff 和 dns/block 特殊出站,这里改用规则动作。
+ { action: 'sniff' },
+ { protocol: 'dns', action: 'hijack-dns' }
],
auto_detect_interface: true,
// 最终兜底规则:
@@ -202,7 +193,6 @@ export function generateSingboxConfig(game: Game, node: NodeConfig, dnsOptions?:
// 说明:NAT1 取决于公网直连能力,客户端侧只能做“更开放映射”的优化,无法绝对保证。
endpoint_independent_nat: true,
stack: sessionTuning.tunStack,
- sniff: true // 开启流量嗅探,用于域名解析
})
}
@@ -218,7 +208,6 @@ export function generateSingboxConfig(game: Game, node: NodeConfig, dnsOptions?:
tag: 'http-in',
listen: '127.0.0.1',
listen_port: httpPort,
- sniff: true,
set_system_proxy: false
})
@@ -227,8 +216,7 @@ export function generateSingboxConfig(game: Game, node: NodeConfig, dnsOptions?:
type: 'socks',
tag: 'socks-in',
listen: '127.0.0.1',
- listen_port: socksPort,
- sniff: true
+ listen_port: socksPort
})
// 配置本地代理的流量出口
@@ -262,7 +250,6 @@ export function generateSingboxConfig(game: Game, node: NodeConfig, dnsOptions?:
tag: 'system-http-in',
listen: '127.0.0.1',
listen_port: httpPort,
- sniff: true,
set_system_proxy: false // 注意:这里仅开启端口,系统代理的设置由外部程序控制
})