Bundle TAP-Windows installer and persist startup config
This commit is contained in:
@@ -39,4 +39,16 @@ export const tapTraffic = (tapDevice) => nativeCall('tap_traffic', { request: {
|
||||
export const resolveTapAdapter = (tapDevice) => nativeCall('resolve_tap_adapter', { request: { tapDevice } }, () => {
|
||||
throw new Error('浏览器预览无法识别 TAP 网卡')
|
||||
})
|
||||
export const startupStatus = () => nativeCall('startup_status', {}, () => ({
|
||||
configExists: false,
|
||||
firstLaunch: false,
|
||||
configPath: '',
|
||||
tapDriverPresent: true,
|
||||
tapInstallerPath: null,
|
||||
}))
|
||||
export const readConfig = () => nativeCall('read_config', {}, () => null)
|
||||
export const writeConfig = (contents) => nativeCall('write_config', { contents }, () => null)
|
||||
export const openTapInstaller = () => nativeCall('open_tap_installer', {}, () => {
|
||||
throw new Error('浏览器预览无法打开 TAP-Windows 安装程序')
|
||||
})
|
||||
export const exportLog = (contents) => nativeCall('export_log', { contents }, () => null)
|
||||
|
||||
+96
-17
@@ -4,7 +4,10 @@ import {
|
||||
ArrowDown, ArrowUp, CheckCircle2, ChevronDown, CircleAlert, Gauge, Globe2, Network, Play,
|
||||
Download, Router, ScrollText, Server, Settings2, Square, Wifi,
|
||||
} from 'lucide-react'
|
||||
import { exportLog, isTauri, resolveTapAdapter, serviceStatus, startService, stopService, tapTraffic } from './lib/tauri'
|
||||
import {
|
||||
exportLog, isTauri, openTapInstaller, readConfig, resolveTapAdapter, serviceStatus, startService,
|
||||
startupStatus, stopService, tapTraffic, writeConfig,
|
||||
} from './lib/tauri'
|
||||
import './styles.css'
|
||||
|
||||
const initialEdge = {
|
||||
@@ -20,22 +23,28 @@ const initialEdge = {
|
||||
|
||||
const edgeConfigStorageKey = 'super-n2n.edge-config.v1'
|
||||
|
||||
function loadEdgeConfig() {
|
||||
function parseEdgeConfig(value) {
|
||||
const fallback = { ...initialEdge }
|
||||
if (typeof window === 'undefined') return fallback
|
||||
try {
|
||||
const saved = JSON.parse(window.localStorage.getItem(edgeConfigStorageKey) || 'null')
|
||||
if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return fallback
|
||||
for (const [field, defaultValue] of Object.entries(initialEdge)) {
|
||||
if (typeof saved[field] === typeof defaultValue) fallback[field] = saved[field]
|
||||
}
|
||||
} catch {
|
||||
// A damaged or unavailable browser store must not block the connection form.
|
||||
const saved = value && typeof value === 'object' && !Array.isArray(value) && value.edge
|
||||
&& typeof value.edge === 'object' ? value.edge : value
|
||||
if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return fallback
|
||||
for (const [field, defaultValue] of Object.entries(initialEdge)) {
|
||||
if (typeof saved[field] === typeof defaultValue) fallback[field] = saved[field]
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function saveEdgeConfig(config) {
|
||||
function loadBrowserEdgeConfig() {
|
||||
if (typeof window === 'undefined') return { ...initialEdge }
|
||||
try {
|
||||
return parseEdgeConfig(JSON.parse(window.localStorage.getItem(edgeConfigStorageKey) || 'null'))
|
||||
} catch {
|
||||
// A damaged or unavailable browser store must not block the connection form.
|
||||
return { ...initialEdge }
|
||||
}
|
||||
}
|
||||
|
||||
function saveBrowserEdgeConfig(config) {
|
||||
try {
|
||||
window.localStorage.setItem(edgeConfigStorageKey, JSON.stringify(config))
|
||||
} catch {
|
||||
@@ -43,9 +52,13 @@ function saveEdgeConfig(config) {
|
||||
}
|
||||
}
|
||||
|
||||
function serializeEdgeConfig(config) {
|
||||
return JSON.stringify({ version: 1, edge: config }, null, 2)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [home, setHome] = useState('edge')
|
||||
const [edge, setEdge] = useState(loadEdgeConfig)
|
||||
const [edge, setEdge] = useState(loadBrowserEdgeConfig)
|
||||
const [edgeRunning, setEdgeRunning] = useState(false)
|
||||
const [activeTapDevice, setActiveTapDevice] = useState('')
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
@@ -53,7 +66,10 @@ function App() {
|
||||
const [logs, setLogs] = useState([])
|
||||
const [traffic, setTraffic] = useState(null)
|
||||
const [message, setMessage] = useState(null)
|
||||
const [startupInfo, setStartupInfo] = useState(null)
|
||||
const [configReady, setConfigReady] = useState(!isTauri)
|
||||
const lastTapSample = useRef(null)
|
||||
const configWriteFailed = useRef(false)
|
||||
|
||||
const appendLog = (text, level = 'info') => {
|
||||
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
@@ -61,8 +77,47 @@ function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
saveEdgeConfig(edge)
|
||||
}, [edge])
|
||||
if (!configReady) return
|
||||
if (!isTauri) {
|
||||
saveBrowserEdgeConfig(edge)
|
||||
return
|
||||
}
|
||||
writeConfig(serializeEdgeConfig(edge)).catch((error) => {
|
||||
if (configWriteFailed.current) return
|
||||
configWriteFailed.current = true
|
||||
appendLog('配置保存失败:' + error.message, 'warning')
|
||||
})
|
||||
}, [edge, configReady])
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
const loadStartup = async () => {
|
||||
try {
|
||||
const status = await startupStatus()
|
||||
if (!active) return
|
||||
setStartupInfo(status)
|
||||
if (isTauri && status.configExists) {
|
||||
const contents = await readConfig()
|
||||
if (active && contents) setEdge(parseEdgeConfig(JSON.parse(contents)))
|
||||
}
|
||||
if (active) {
|
||||
setConfigReady(true)
|
||||
if (status.firstLaunch) {
|
||||
appendLog(
|
||||
status.tapDriverPresent ? '首次启动:已检测到 TAP-Windows 网卡' : '首次启动:未检测到 TAP-Windows 网卡',
|
||||
status.tapDriverPresent ? 'success' : 'warning',
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!active) return
|
||||
setConfigReady(true)
|
||||
appendLog('读取启动配置失败:' + error.message, 'warning')
|
||||
}
|
||||
}
|
||||
loadStartup()
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
@@ -163,6 +218,17 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const installTapDriver = async () => {
|
||||
try {
|
||||
const path = await openTapInstaller()
|
||||
appendLog('已打开 TAP-Windows 安装程序:' + path, 'info')
|
||||
notify('请按安装向导完成驱动安装,然后重新打开应用', 'neutral')
|
||||
} catch (error) {
|
||||
appendLog('打开 TAP-Windows 安装程序失败:' + error.message, 'error')
|
||||
notify('无法打开驱动安装程序:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return <main className="app-shell">
|
||||
<header className="app-header">
|
||||
<div className="brand"><span className="brand-icon"><Network size={19} /></span><span>Super N2N</span></div>
|
||||
@@ -180,7 +246,7 @@ function App() {
|
||||
</div>
|
||||
|
||||
{home === 'edge'
|
||||
? <EdgeHome edge={edge} updateEdge={updateEdge} running={edgeRunning} traffic={traffic} activeTapDevice={activeTapDevice} advancedOpen={advancedOpen} setAdvancedOpen={setAdvancedOpen} logOpen={logOpen} setLogOpen={setLogOpen} logs={logs} onToggle={toggleEdge} onExportLog={() => exportLogs(logs, notify)} />
|
||||
? <EdgeHome edge={edge} updateEdge={updateEdge} running={edgeRunning} traffic={traffic} activeTapDevice={activeTapDevice} startupInfo={startupInfo} advancedOpen={advancedOpen} setAdvancedOpen={setAdvancedOpen} logOpen={logOpen} setLogOpen={setLogOpen} logs={logs} onToggle={toggleEdge} onInstallTap={installTapDriver} onExportLog={() => exportLogs(logs, notify)} />
|
||||
: <SupernodeHome />}
|
||||
</section>
|
||||
|
||||
@@ -189,7 +255,7 @@ function App() {
|
||||
</main>
|
||||
}
|
||||
|
||||
function EdgeHome({ edge, updateEdge, running, traffic, activeTapDevice, advancedOpen, setAdvancedOpen, logOpen, setLogOpen, logs, onToggle, onExportLog }) {
|
||||
function EdgeHome({ edge, updateEdge, running, traffic, activeTapDevice, startupInfo, advancedOpen, setAdvancedOpen, setLogOpen, logOpen, logs, onToggle, onInstallTap, onExportLog }) {
|
||||
const toggleAdvanced = () => {
|
||||
setLogOpen(false)
|
||||
setAdvancedOpen((current) => !current)
|
||||
@@ -232,6 +298,8 @@ function EdgeHome({ edge, updateEdge, running, traffic, activeTapDevice, advance
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TapSetupPrompt status={startupInfo} onInstall={onInstallTap} />
|
||||
|
||||
<button className={running ? 'connect-button stop' : 'connect-button'} onClick={onToggle}>
|
||||
{running ? <Square size={19} fill="currentColor" /> : <Play size={20} fill="currentColor" />}
|
||||
{running ? '断开连接' : '连接网络'}
|
||||
@@ -257,6 +325,17 @@ function EdgeHome({ edge, updateEdge, running, traffic, activeTapDevice, advance
|
||||
</div>
|
||||
}
|
||||
|
||||
function TapSetupPrompt({ status, onInstall }) {
|
||||
if (!status?.firstLaunch || status.tapDriverPresent) return null
|
||||
return <aside className="tap-setup" role="alert">
|
||||
<div className="tap-setup-copy">
|
||||
<CircleAlert size={16} />
|
||||
<span><strong>需要安装 TAP-Windows</strong><small>未检测到虚拟网卡,安装后重新打开应用。</small></span>
|
||||
</div>
|
||||
{status.tapInstallerPath && <button className="tap-install" onClick={onInstall} title="打开随包 TAP-Windows 安装程序"><Download size={14} />安装</button>}
|
||||
</aside>
|
||||
}
|
||||
|
||||
function TrafficStrip({ traffic, tapDevice }) {
|
||||
if (!traffic || traffic.error) {
|
||||
return <div className="traffic-strip pending" role="status" title={traffic?.error || ''}>
|
||||
|
||||
@@ -64,6 +64,15 @@ h1 { margin: 0; color: var(--ink); font-size: 24px; line-height: 1.2; letter-spa
|
||||
.ip-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.input-group small { color: var(--muted); font-size: 12px; line-height: 1.4; }
|
||||
|
||||
.tap-setup { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; margin: 0 22px 10px; padding: 8px 9px; color: #855f16; background: #fff8e8; border: 1px solid #ecd59c; border-radius: 5px; }
|
||||
.tap-setup-copy { min-width: 0; display: flex; align-items: flex-start; gap: 7px; }
|
||||
.tap-setup-copy > svg { flex: 0 0 auto; margin-top: 1px; }
|
||||
.tap-setup-copy > span { min-width: 0; display: grid; gap: 2px; }
|
||||
.tap-setup-copy strong { color: #76500d; font-size: 12px; line-height: 1.2; }
|
||||
.tap-setup-copy small { overflow: hidden; color: #927642; font-size: 11px; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tap-install { min-height: 28px; display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 0 8px; color: #76500d; background: #fff; border: 1px solid #dfc57d; border-radius: 4px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||
.tap-install:hover { background: #fffdf5; border-color: #caa64d; }
|
||||
|
||||
.dhcp-toggle { display: inline-flex; align-items: center; gap: 7px; min-height: 28px; color: var(--muted); font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
.dhcp-toggle input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.toggle-track { position: relative; width: 36px; height: 22px; flex: 0 0 auto; background: #a8b7ba; border-radius: 14px; transition: background 180ms ease; }
|
||||
@@ -153,6 +162,7 @@ h1 { margin: 0; color: var(--ink); font-size: 24px; line-height: 1.2; letter-spa
|
||||
.input-group { padding: 12px 0; }
|
||||
.server-inputs { grid-template-columns: minmax(0, 1fr) auto 74px; gap: 5px; }
|
||||
.input-group input { height: 38px; font-size: 14px; }
|
||||
.tap-setup { margin: 0 16px 8px; }
|
||||
.connect-button { width: calc(100% - 32px); margin: 10px 16px 14px; }
|
||||
.connection-summary { grid-template-columns: 1fr; }
|
||||
.connection-summary div { grid-template-columns: 80px 1fr; align-items: baseline; padding: 9px 14px; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
@@ -194,6 +204,13 @@ h1 { margin: 0; color: var(--ink); font-size: 24px; line-height: 1.2; letter-spa
|
||||
.field-title { gap: 0; font-size: 12px; }
|
||||
.field-title svg { display: none; }
|
||||
.input-group input { height: 30px; padding: 0 7px; border-radius: 4px; font-size: 12px; }
|
||||
.tap-setup { gap: 5px; margin: 0 8px 3px; padding: 6px 7px; border-radius: 4px; }
|
||||
.tap-setup-copy { gap: 5px; }
|
||||
.tap-setup-copy > svg { width: 14px; height: 14px; }
|
||||
.tap-setup-copy strong { font-size: 10px; }
|
||||
.tap-setup-copy small { font-size: 9px; }
|
||||
.tap-install { min-height: 24px; gap: 3px; padding: 0 6px; font-size: 10px; }
|
||||
.tap-install svg { width: 12px; height: 12px; }
|
||||
.server-inputs { grid-template-columns: minmax(0, 1fr) auto 58px; gap: 4px; }
|
||||
.port-separator { font-size: 15px; }
|
||||
.input-group small { display: none; }
|
||||
|
||||
Reference in New Issue
Block a user