's'
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "live-recorder-console",
|
||||
"productName": "直播录制助手",
|
||||
"productName": "无人直播助手",
|
||||
"version": "1.0.0",
|
||||
"description": "直播录制助手(Windows 懒人包)",
|
||||
"description": "无人直播助手",
|
||||
"main": "./out/main/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.douyin.recorder.console",
|
||||
"productName": "直播录制控制台",
|
||||
"productName": "无人直播助手",
|
||||
"directories": {
|
||||
"output": "dist",
|
||||
"buildResources": "build"
|
||||
|
||||
@@ -57,7 +57,7 @@ export function applyAppHostBootstrap(getBackendRoot: () => string): void {
|
||||
}
|
||||
|
||||
const ver = app.getVersion()
|
||||
const desc = readPackageJsonField('description') || '直播录制与转播控制台'
|
||||
const desc = readPackageJsonField('description') || '无人直播与转播控制台'
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: app.getName(),
|
||||
applicationVersion: ver,
|
||||
@@ -85,14 +85,6 @@ export function applyAppHostBootstrap(getBackendRoot: () => string): void {
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
app.setUserTasks([
|
||||
{
|
||||
program: process.execPath,
|
||||
arguments: '--nav=desk',
|
||||
title: '工作台',
|
||||
description: '打开工作台仪表盘',
|
||||
iconPath: process.execPath,
|
||||
iconIndex: 0,
|
||||
},
|
||||
{
|
||||
program: process.execPath,
|
||||
arguments: '--nav=record',
|
||||
|
||||
@@ -181,7 +181,7 @@ function resolveWeb2Host(): string {
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return '127.0.0.1'
|
||||
return '0.0.0.0'
|
||||
}
|
||||
|
||||
function resolveWeb2ApiTokenForChild(): string | undefined {
|
||||
@@ -323,7 +323,7 @@ function startPythonBackend(backendRoot: string, pythonExe: string): void {
|
||||
const apiTok = resolveWeb2ApiTokenForChild()
|
||||
if (web2Host === '0.0.0.0' && !apiTok) {
|
||||
console.warn(
|
||||
'[直播录制助手] web2 监听 0.0.0.0 但未设置 WEB2_API_TOKEN 或 config/web2_api_token.txt,API 无鉴权,请勿暴露到公网。',
|
||||
'[无人直播助手] web2 监听 0.0.0.0 但未设置 WEB2_API_TOKEN 或 config/web2_api_token.txt,API 无鉴权,请勿暴露到公网。',
|
||||
)
|
||||
}
|
||||
const env = {
|
||||
@@ -351,7 +351,7 @@ function startPythonBackend(backendRoot: string, pythonExe: string): void {
|
||||
pythonChild.on('close', (code) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||
if (code !== 0 && code !== null) {
|
||||
dialog.showErrorBox('直播录制助手', `软件内部服务已停止运行。\n\n${stderrTail()}`)
|
||||
dialog.showErrorBox('无人直播助手', `软件内部服务已停止运行。\n\n${stderrTail()}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -391,7 +391,7 @@ async function createWindow(): Promise<void> {
|
||||
minHeight: 560,
|
||||
show: false,
|
||||
backgroundColor: '#14161c',
|
||||
title: '直播录制助手',
|
||||
title: '无人直播助手',
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: resolvePreload(),
|
||||
@@ -413,7 +413,7 @@ async function createWindow(): Promise<void> {
|
||||
try {
|
||||
await waitForDevServer(devUrl)
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('直播录制助手', e instanceof Error ? e.message : String(e))
|
||||
dialog.showErrorBox('无人直播助手', e instanceof Error ? e.message : String(e))
|
||||
mainWindow.destroy()
|
||||
mainWindow = null
|
||||
return
|
||||
@@ -431,7 +431,7 @@ async function createWindow(): Promise<void> {
|
||||
} catch {
|
||||
if (i === 7) {
|
||||
dialog.showErrorBox(
|
||||
'直播录制助手',
|
||||
'无人直播助手',
|
||||
`无法加载开发页面:${devUrl}\n请确认 Vite 已启动且端口未被占用。`,
|
||||
)
|
||||
mainWindow.destroy()
|
||||
@@ -442,7 +442,7 @@ async function createWindow(): Promise<void> {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('直播录制助手', e instanceof Error ? e.message : String(e))
|
||||
dialog.showErrorBox('无人直播助手', e instanceof Error ? e.message : String(e))
|
||||
mainWindow.destroy()
|
||||
mainWindow = null
|
||||
return
|
||||
@@ -655,6 +655,9 @@ if (!gotLock) {
|
||||
if (typeof enabled !== 'boolean') {
|
||||
return { ok: false, error: '参数无效' }
|
||||
}
|
||||
if (enabled === false) {
|
||||
return { ok: false, error: '本应用固定开启局域网 / 虚拟网访问,无法关闭。' }
|
||||
}
|
||||
const br = _backendRoot ?? getBackendRoot()
|
||||
const cfgDir = path.join(br, 'config')
|
||||
fs.mkdirSync(cfgDir, { recursive: true })
|
||||
@@ -685,7 +688,7 @@ if (!gotLock) {
|
||||
|
||||
if (!fs.existsSync(path.join(backendRoot, 'web2.py'))) {
|
||||
dialog.showErrorBox(
|
||||
'直播录制助手',
|
||||
'无人直播助手',
|
||||
`未找到程序必要文件,当前安装可能不完整。\n\n路径:${backendRoot}\n\n开发调试:请在项目根目录保留 web2.py,并在 electron 目录执行 pnpm dev。`,
|
||||
)
|
||||
app.quit()
|
||||
@@ -698,7 +701,7 @@ if (!gotLock) {
|
||||
await waitForHealth()
|
||||
} catch (e) {
|
||||
dialog.showErrorBox(
|
||||
'直播录制助手',
|
||||
'无人直播助手',
|
||||
`软件未能正常启动。\n\n${e instanceof Error ? e.message : String(e)}`,
|
||||
)
|
||||
killPythonTree()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<title>直播录制助手</title>
|
||||
<title>无人直播助手</title>
|
||||
<script>
|
||||
try {
|
||||
var t = localStorage.getItem('theme')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 直播录制助手 — 借鉴 Clash Verge 类客户端:深色侧栏 + 主工作区(离线无外链字体) */
|
||||
/* 无人直播助手 — 借鉴 Clash Verge 类客户端:深色侧栏 + 主工作区(离线无外链字体) */
|
||||
|
||||
:root {
|
||||
--font-ui: "Segoe UI", "Microsoft YaHei UI", "PingFang SC", system-ui, sans-serif;
|
||||
|
||||
@@ -70,7 +70,7 @@ function SidebarBrand({
|
||||
<div className="app-sidebar__logo" aria-hidden />
|
||||
<div className="app-sidebar__brand-main">
|
||||
<div className="app-sidebar__brand-title-row">
|
||||
<span className="app-sidebar__app-name">直播录制助手</span>
|
||||
<span className="app-sidebar__app-name">无人直播助手</span>
|
||||
<button
|
||||
type="button"
|
||||
className="app-sidebar__theme-toggle"
|
||||
@@ -81,7 +81,7 @@ function SidebarBrand({
|
||||
{theme === 'dark' ? <ThemeIconSun /> : <ThemeIconMoon />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="app-sidebar__app-tag">{tag}</div>
|
||||
{tag ? <div className="app-sidebar__app-tag">{tag}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -96,6 +96,9 @@ const NAV_ITEMS: { id: NavId; label: string; hint: string }[] = [
|
||||
{ id: 'wechat', label: '微信', hint: 'Chatbot 与扫码' },
|
||||
]
|
||||
|
||||
/** 侧栏不展示「工作台」「微信」,路由仍保留供内部/深链使用 */
|
||||
const SIDEBAR_NAV_ITEMS = NAV_ITEMS.filter((n) => n.id !== 'desk' && n.id !== 'wechat')
|
||||
|
||||
type StatusVariant =
|
||||
| 'loading'
|
||||
| 'online'
|
||||
@@ -207,17 +210,17 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
const h = window.appHost
|
||||
if (!h?.onNavigate) return
|
||||
return h.onNavigate((nav) => {
|
||||
if (
|
||||
nav === 'desk' ||
|
||||
nav === 'record' ||
|
||||
nav === 'network' ||
|
||||
nav === 'proxy' ||
|
||||
nav === 'system' ||
|
||||
nav === 'wechat'
|
||||
) {
|
||||
setNav(nav)
|
||||
}
|
||||
return h.onNavigate((raw) => {
|
||||
const nav =
|
||||
raw === 'desk' || raw === 'wechat'
|
||||
? 'record'
|
||||
: raw === 'record' ||
|
||||
raw === 'network' ||
|
||||
raw === 'proxy' ||
|
||||
raw === 'system'
|
||||
? raw
|
||||
: null
|
||||
if (nav) setNav(nav)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -684,7 +687,7 @@ export default function App() {
|
||||
return (
|
||||
<div className="app-frame app-frame--verge">
|
||||
<aside className="app-sidebar" aria-label="主导航">
|
||||
<SidebarBrand tag="本地控制台" theme={theme} applyTheme={applyTheme} />
|
||||
<SidebarBrand tag="" theme={theme} applyTheme={applyTheme} />
|
||||
</aside>
|
||||
<div className="app-workspace">
|
||||
<div className="app-workspace__inner app-workspace__inner--center">
|
||||
@@ -708,9 +711,9 @@ export default function App() {
|
||||
return (
|
||||
<div className="app-frame app-frame--verge">
|
||||
<aside className="app-sidebar" aria-label="主导航">
|
||||
<SidebarBrand tag="懒人包 · 本地" theme={theme} applyTheme={applyTheme} />
|
||||
<SidebarBrand tag="" theme={theme} applyTheme={applyTheme} />
|
||||
<nav className="app-sidebar__nav">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
{SIDEBAR_NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
|
||||
@@ -5,43 +5,18 @@ type LoginItem = {
|
||||
executableWillLaunchAtLogin?: boolean
|
||||
}
|
||||
|
||||
type PathInfo = {
|
||||
userData: string
|
||||
logs: string
|
||||
temp: string
|
||||
backendRoot: string
|
||||
backendConfig: string
|
||||
}
|
||||
|
||||
type ProcessMetric = {
|
||||
pid?: number
|
||||
type?: string
|
||||
name?: string
|
||||
cpu?: { percentUsage?: number }
|
||||
memory?: number
|
||||
}
|
||||
|
||||
export default function AppHostControls() {
|
||||
const host = typeof window !== 'undefined' ? window.appHost : undefined
|
||||
const [login, setLogin] = useState<LoginItem | null>(null)
|
||||
const [paths, setPaths] = useState<PathInfo | null>(null)
|
||||
const [metrics, setMetrics] = useState<ProcessMetric[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [badgeDraft, setBadgeDraft] = useState(0)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!host?.getPaths) return
|
||||
if (!host?.getLoginItemSettings) return
|
||||
setErr(null)
|
||||
try {
|
||||
const [li, p, m] = await Promise.all([
|
||||
host.getLoginItemSettings?.() ?? Promise.resolve(null),
|
||||
host.getPaths(),
|
||||
host.getMetrics?.() ?? Promise.resolve([]),
|
||||
])
|
||||
const li = await (host.getLoginItemSettings?.() ?? Promise.resolve(null))
|
||||
setLogin((li as LoginItem) ?? null)
|
||||
setPaths(p as PathInfo)
|
||||
setMetrics(Array.isArray(m) ? (m as ProcessMetric[]) : [])
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
@@ -75,7 +50,7 @@ export default function AppHostControls() {
|
||||
客户端与系统集成
|
||||
</h2>
|
||||
<p className="card__desc">
|
||||
基于 Electron <code className="remote-code">app</code>:开机启动、任务栏快捷、关于面板、日志目录、进程指标等。
|
||||
基于 Electron <code className="remote-code">app</code>:开机启动、任务栏快捷、关于面板、日志目录等。
|
||||
</p>
|
||||
{err && (
|
||||
<p className="remote-error" role="alert">
|
||||
@@ -133,93 +108,9 @@ export default function AppHostControls() {
|
||||
</button>
|
||||
</div>
|
||||
<p className="app-host-hint">
|
||||
Windows:固定到任务栏后,可右键图标使用「工作台 / 录制 / 网络」快捷任务。
|
||||
Windows:固定到任务栏后,可右键图标使用「录制 / 网络」快捷任务。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="app-host-block">
|
||||
<div className="app-host-block__title">Dock 角标(macOS / 部分 Linux)</div>
|
||||
<div className="app-host-badge-row">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={99}
|
||||
className="app-host-badge-input"
|
||||
value={badgeDraft}
|
||||
onChange={(e) => setBadgeDraft(Number(e.target.value) || 0)}
|
||||
aria-label="角标数字"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => void host.setBadge?.(badgeDraft)}
|
||||
>
|
||||
应用角标
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => void host.setBadge?.(0)}>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
<p className="app-host-hint">Windows 无 Dock 角标;可与后续「断流告警」计数联动。</p>
|
||||
</div>
|
||||
|
||||
<div className="app-host-block">
|
||||
<div className="app-host-block__title">路径(便于打包日志给技术支持)</div>
|
||||
{paths && (
|
||||
<ul className="app-host-paths">
|
||||
<li>
|
||||
<span className="app-host-paths__k">用户数据</span>
|
||||
<code className="app-host-paths__v">{paths.userData}</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="app-host-paths__k">日志</span>
|
||||
<code className="app-host-paths__v">{paths.logs}</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="app-host-paths__k">后端根目录</span>
|
||||
<code className="app-host-paths__v">{paths.backendRoot}</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="app-host-paths__k">config</span>
|
||||
<code className="app-host-paths__v">{paths.backendConfig}</code>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-host-block app-host-block--wide">
|
||||
<div className="app-host-block__title">Electron 进程指标(app.getAppMetrics)</div>
|
||||
{metrics.length > 0 ? (
|
||||
<div className="metrics-table-wrap">
|
||||
<table className="metrics-table app-host-metrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>PID</th>
|
||||
<th>名称</th>
|
||||
<th>CPU</th>
|
||||
<th>内存</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metrics.map((row, i) => (
|
||||
<tr key={`${row.pid ?? i}-${row.type ?? ''}`}>
|
||||
<td>{row.type ?? '—'}</td>
|
||||
<td>{row.pid ?? '—'}</td>
|
||||
<td>{row.name ?? '—'}</td>
|
||||
<td>
|
||||
{row.cpu?.percentUsage != null ? `${row.cpu.percentUsage.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td>{row.memory != null ? `${(row.memory / 1024 / 1024).toFixed(1)} MB` : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="card__desc">暂无数据</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -68,7 +68,15 @@ function fmtMbps(v: number | null | undefined): string {
|
||||
return `${v.toFixed(2)} Mbps`
|
||||
}
|
||||
|
||||
function SiteRow({ title, site }: { title: string; site: SiteInfo }) {
|
||||
function SiteRow({
|
||||
title,
|
||||
site,
|
||||
speed,
|
||||
}: {
|
||||
title: string
|
||||
site: SiteInfo
|
||||
speed: SpeedBlock
|
||||
}) {
|
||||
const ok = site.reachable
|
||||
return (
|
||||
<div className="net-site">
|
||||
@@ -83,6 +91,18 @@ function SiteRow({ title, site }: { title: string; site: SiteInfo }) {
|
||||
<span className="net-v">
|
||||
{site.latency_ms != null && site.latency_ms >= 0 ? `${site.latency_ms.toFixed(0)} ms` : '—'}
|
||||
</span>
|
||||
<span className="net-k">下行(估算)</span>
|
||||
<span className="net-v">{fmtMbps(speed.download_mbps)}</span>
|
||||
<span className="net-k">上行(估算)</span>
|
||||
<span className="net-v">{fmtMbps(speed.upload_mbps)}</span>
|
||||
{(speed.download_error || speed.upload_error) && (
|
||||
<>
|
||||
<span className="net-k">测速说明</span>
|
||||
<span className="net-v">
|
||||
{[speed.download_error, speed.upload_error].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="net-k">解析 IP</span>
|
||||
<span className="net-v net-v--mono">{site.resolved_ip ?? '—'}</span>
|
||||
<span className="net-k">地区(参考)</span>
|
||||
@@ -141,7 +161,7 @@ export default function NetworkDashboard() {
|
||||
网络仪表盘
|
||||
</h2>
|
||||
<p className="card__desc net-dash__lead">
|
||||
检测 Google(偏海外)与抖音(偏国内)是否通畅、解析出口是否像两条线,并估算上下行(采样约 0.6MB
|
||||
检测 Google(海外参考)与抖音(国内参考)是否通畅,并在各自卡片内展示延迟、解析与上下行估算(采样约 0.6MB
|
||||
下行 / 256KB 上行)。
|
||||
</p>
|
||||
</div>
|
||||
@@ -158,75 +178,14 @@ export default function NetworkDashboard() {
|
||||
|
||||
<>
|
||||
<div className="net-site-row">
|
||||
<SiteRow title="Google(海外参考)" site={view.google} />
|
||||
<SiteRow title="抖音(国内参考)" site={view.douyin} />
|
||||
<SiteRow title="Google(海外参考)" site={view.google} speed={view.speed.overseas} />
|
||||
<SiteRow title="抖音(国内参考)" site={view.douyin} speed={view.speed.domestic} />
|
||||
</div>
|
||||
|
||||
<div className="net-routes">
|
||||
<span className="net-routes__label">线路是否像「两条线」</span>
|
||||
<span
|
||||
className={`net-routes__pill ${
|
||||
view.routes.different === true
|
||||
? 'net-routes__pill--yes'
|
||||
: view.routes.different === false
|
||||
? 'net-routes__pill--no'
|
||||
: 'net-routes__pill--unk'
|
||||
}`}
|
||||
>
|
||||
{view.routes.different === true
|
||||
? '是(解析国家不同)'
|
||||
: view.routes.different === false
|
||||
? '否或不确定'
|
||||
: '未知'}
|
||||
</span>
|
||||
<p className="net-routes__note">{view.routes.note}</p>
|
||||
</div>
|
||||
|
||||
<div className="net-speed">
|
||||
<h3 className="net-speed__title">网速(估算)</h3>
|
||||
<div className="net-speed__grid">
|
||||
<div className="net-speed__col">
|
||||
<div className="net-speed__head">海外</div>
|
||||
<div className="net-metric">
|
||||
<span className="net-metric__k">下行</span>
|
||||
<span className="net-metric__v">{fmtMbps(view.speed.overseas.download_mbps)}</span>
|
||||
</div>
|
||||
<div className="net-metric">
|
||||
<span className="net-metric__k">上行</span>
|
||||
<span className="net-metric__v">{fmtMbps(view.speed.overseas.upload_mbps)}</span>
|
||||
</div>
|
||||
{(view.speed.overseas.download_error || view.speed.overseas.upload_error) && (
|
||||
<p className="net-speed__hint">
|
||||
{[view.speed.overseas.download_error, view.speed.overseas.upload_error]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="net-speed__col">
|
||||
<div className="net-speed__head">国内</div>
|
||||
<div className="net-metric">
|
||||
<span className="net-metric__k">下行</span>
|
||||
<span className="net-metric__v">{fmtMbps(view.speed.domestic.download_mbps)}</span>
|
||||
</div>
|
||||
<div className="net-metric">
|
||||
<span className="net-metric__k">上行</span>
|
||||
<span className="net-metric__v">{fmtMbps(view.speed.domestic.upload_mbps)}</span>
|
||||
</div>
|
||||
{(view.speed.domestic.download_error || view.speed.domestic.upload_error) && (
|
||||
<p className="net-speed__hint">
|
||||
{[view.speed.domestic.download_error, view.speed.domestic.upload_error]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="net-speed__foot">
|
||||
上行若失败多为网站不接受测试流量,不代表您无法上传直播;海外项走 Google / httpbin,国内项走抖音首页流与门户
|
||||
POST。
|
||||
</p>
|
||||
</div>
|
||||
<p className="net-speed__foot net-dash__speed-foot">
|
||||
上行若失败多为网站不接受测试流量,不代表您无法上传直播;海外项走 Google / httpbin,国内项走抖音首页流与门户
|
||||
POST。
|
||||
</p>
|
||||
</>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import QRCode from 'qrcode'
|
||||
import { initApiFromMain } from './api'
|
||||
|
||||
@@ -32,15 +32,12 @@ export default function RemoteBinding() {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [msg, setMsg] = useState('')
|
||||
const [selectedIp, setSelectedIp] = useState('')
|
||||
const [manualHost, setManualHost] = useState('')
|
||||
const [qrImg, setQrImg] = useState<string | null>(null)
|
||||
const [bindPayload, setBindPayload] = useState<Record<string, unknown> | null>(null)
|
||||
|
||||
const pickBindIp = useCallback((): string => {
|
||||
if (!status?.candidates?.length) return manualHost.trim()
|
||||
if (selectedIp.trim()) return selectedIp.trim()
|
||||
return manualHost.trim()
|
||||
}, [status?.candidates?.length, selectedIp, manualHost])
|
||||
return selectedIp.trim()
|
||||
}, [selectedIp])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!rb?.getStatus) {
|
||||
@@ -61,6 +58,32 @@ export default function RemoteBinding() {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
const autoEnableRemoteOnce = useRef(false)
|
||||
|
||||
/** 默认开启局域网/虚拟网访问,且界面不允许关闭 */
|
||||
useEffect(() => {
|
||||
if (!rb?.setRemoteEnabled || !status) return
|
||||
if (status.envHostOverride) return
|
||||
if (status.remoteEnabled) return
|
||||
if (autoEnableRemoteOnce.current) return
|
||||
autoEnableRemoteOnce.current = true
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const r = await rb.setRemoteEnabled(true)
|
||||
if (!r.ok || cancelled) return
|
||||
await initApiFromMain()
|
||||
await load()
|
||||
} finally {
|
||||
if (!cancelled) setBusy(false)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [rb, status?.remoteEnabled, status?.envHostOverride, load])
|
||||
|
||||
useEffect(() => {
|
||||
if (!status?.candidates?.length) return
|
||||
setSelectedIp((prev) => {
|
||||
@@ -97,23 +120,6 @@ export default function RemoteBinding() {
|
||||
void QRCode.toDataURL(json, { margin: 2, width: 224 }).then(setQrImg).catch(() => setQrImg(null))
|
||||
}, [bindPayload])
|
||||
|
||||
const onToggle = async (enabled: boolean) => {
|
||||
if (!rb?.setRemoteEnabled) return
|
||||
setBusy(true)
|
||||
setMsg('')
|
||||
try {
|
||||
const r = await rb.setRemoteEnabled(enabled)
|
||||
if (!r.ok) {
|
||||
setMsg(r.error)
|
||||
return
|
||||
}
|
||||
await initApiFromMain()
|
||||
await load()
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onGenerateToken = async () => {
|
||||
if (!rb?.generateToken) return
|
||||
setBusy(true)
|
||||
@@ -134,7 +140,7 @@ export default function RemoteBinding() {
|
||||
const onGenerateQr = async () => {
|
||||
if (!rb?.getBindPayload) return
|
||||
if (!status?.remoteEnabled) {
|
||||
setMsg('请先勾选「允许局域网 / 虚拟网访问」并等待后端重启完成。')
|
||||
setMsg('请等待局域网 / 虚拟网访问已开启并生效后再试。')
|
||||
return
|
||||
}
|
||||
const ip = pickBindIp()
|
||||
@@ -142,8 +148,8 @@ export default function RemoteBinding() {
|
||||
const hasList = !!status.candidates?.length
|
||||
setMsg(
|
||||
hasList
|
||||
? '请从上方下拉选择本机 IP,或在「手动 IP」中填写手机能访问的地址。'
|
||||
: '未检测到本机局域网 IP:请在下方「手动 IP」中输入(例如 192.168.x.x 或与手机同网段地址、EasyTier 虚拟 IP),或点「刷新 IP 列表」后重试。',
|
||||
? '请从上方下拉选择本机可访问的 IP。'
|
||||
: '未检测到本机局域网 IP:请点「刷新 IP 列表」或检查网卡与虚拟网后再试。',
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -184,25 +190,6 @@ export default function RemoteBinding() {
|
||||
<code className="remote-code">easytier</code> 字段完成配对。开启「允许远程」后监听 0.0.0.0,请务必生成 API 密钥。
|
||||
</p>
|
||||
|
||||
<div className="remote-et-status" role="status">
|
||||
<span className={`remote-et-pill ${s?.easytierBundled ? 'on' : 'off'}`}>
|
||||
{s?.easytierBundled ? '已内置 easytier-core' : '未找到内置程序(开发环境请执行 pnpm run prepare-easytier)'}
|
||||
</span>
|
||||
{s?.easytierBundled && (
|
||||
<span className={`remote-et-pill ${s.easytierRunning ? 'on' : 'warn'}`}>
|
||||
{s.easytierRunning ? 'EasyTier 进程运行中' : '进程未运行或已退出'}
|
||||
</span>
|
||||
)}
|
||||
{!!s?.easytierNetworkName && (
|
||||
<span className="remote-et-meta">组网名:{s.easytierNetworkName}</span>
|
||||
)}
|
||||
</div>
|
||||
{s?.easytierBundled && s.easytierLastError && (
|
||||
<p className="remote-warn" role="status">
|
||||
EasyTier:{s.easytierLastError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status?.envHostOverride && (
|
||||
<p className="remote-warn" role="status">
|
||||
当前由环境变量 WEB2_HOST 覆盖绑定地址,界面开关已禁用。
|
||||
@@ -215,10 +202,10 @@ export default function RemoteBinding() {
|
||||
type="checkbox"
|
||||
className="remote-toggle-input"
|
||||
checked={!!status?.remoteEnabled}
|
||||
disabled={busy || !!status?.envHostOverride}
|
||||
onChange={(e) => void onToggle(e.target.checked)}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
<span>允许局域网 / 虚拟网访问(监听 0.0.0.0)</span>
|
||||
<span>允许局域网 / 虚拟网访问(监听 0.0.0.0,默认开启且不可关闭)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -244,7 +231,7 @@ export default function RemoteBinding() {
|
||||
>
|
||||
{(status?.candidates?.length
|
||||
? status.candidates
|
||||
: [{ address: '', name: '(暂无候选,请用下方手动 IP 或点刷新)' }]
|
||||
: [{ address: '', name: '(暂无候选,请点刷新 IP 列表)' }]
|
||||
).map((c) => (
|
||||
<option key={c.address || 'none'} value={c.address} disabled={!c.address}>
|
||||
{c.address ? `${c.address} — ${c.name}` : c.name}
|
||||
@@ -256,22 +243,6 @@ export default function RemoteBinding() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="remote-row">
|
||||
<label className="field-label" htmlFor="remoteManualIp">
|
||||
手动 IP(无下拉或地址不对时必填)
|
||||
</label>
|
||||
<input
|
||||
id="remoteManualIp"
|
||||
type="text"
|
||||
className="select-process"
|
||||
placeholder="例如 192.168.1.10、10.126.126.1"
|
||||
value={manualHost}
|
||||
onChange={(e) => setManualHost(e.target.value)}
|
||||
disabled={busy}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="remote-row">
|
||||
<button type="button" className="btn btn-primary" disabled={busy} onClick={() => void onGenerateQr()}>
|
||||
生成绑定二维码
|
||||
@@ -280,7 +251,7 @@ export default function RemoteBinding() {
|
||||
</div>
|
||||
|
||||
{!s?.remoteEnabled && (
|
||||
<p className="remote-qr-hint">勾选「允许远程」并生效后,虚拟网内手机可访问本机 API 并扫码绑定。</p>
|
||||
<p className="remote-qr-hint">正在或等待开启局域网访问;生效后虚拟网内手机可访问本机 API 并扫码绑定。</p>
|
||||
)}
|
||||
|
||||
{s?.remoteEnabled && bindPayload && qrImg && (
|
||||
@@ -305,7 +276,7 @@ export default function RemoteBinding() {
|
||||
|
||||
{s?.remoteEnabled && !bindPayload && (
|
||||
<p className="remote-qr-hint">
|
||||
尚未生成绑定数据:在下拉中选局域网 IP,或在「手动 IP」填写后点「生成绑定二维码」;亦可点「刷新 IP 列表」更新网卡地址。
|
||||
尚未生成绑定数据:在下拉中选局域网 IP 后点「生成绑定二维码」,或点「刷新 IP 列表」更新网卡地址。
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user