chore: 保存工作副本——客户端授权新契约(双端点/验签/令牌/周检)与桌面会话可靠性改进

This commit is contained in:
Logan.Li committed 2026-09-14 22:10:22 +08:00
1 parent f71eef0eba
commit ae6093c9b3
163 files changed
+4911 -2494

No files matched your search

+49 -128
View File
@@ -7,21 +7,20 @@ import type { ConnectionState } from '../../stores/connection'
import { useConnectionPanelStore } from '../../stores/connectionPanelStore'
import { useSessionStore, useEntitlementStore } from '../../stores'
import type { HostMetrics } from '../../../shared/types/host-metrics.types'
import { formatLocaleTime, translateErrorMessage } from '../../i18n/format'
import { formatLocaleTime, translateErrorMessage, translateIpcError } from '../../i18n/format'
import { useToast } from '../../hooks/useToast'
import ForwardRuleDrawer, {
DEFAULT_DYNAMIC_PORT,
emptyForwardDraft,
resolveDynamicPort,
} from '../connection/ForwardRuleDrawer'
import ForwardRuleDrawer, { emptyForwardDraft } from '../connection/ForwardRuleDrawer'
import {
DEFAULT_DYNAMIC_PORT,
parseForwardRules,
portForwardStorageKey,
resolveDynamicPort,
resolveForwardTargetHost,
resolveLocalBindHost,
resolveRemoteBindHost,
type ForwardRule,
} from '../connection/forwardRules'
import { startNativeForward, stopNativeForward } from '../connection/forwardRuntime'
import {
forgetForwardIntent,
forwardIntentKey,
@@ -42,43 +41,43 @@ interface StatusDisplay {
const STATUS_CONFIG: Record<string, Omit<StatusDisplay, 'labelKey'> & { labelKey: string }> = {
connected: {
icon: <Wifi className="w-3 h-3" />,
icon: <Wifi className="h-3 w-3" />,
labelKey: 'status.connected',
colorClass: 'text-feedback-success',
animateClass: '',
},
connecting: {
icon: <Loader2 className="w-3 h-3 animate-spin" />,
icon: <Loader2 className="h-3 w-3 animate-spin" />,
labelKey: 'status.connecting',
colorClass: 'text-feedback-warning',
animateClass: 'animate-pulse',
},
background: {
icon: <Wifi className="w-3 h-3" />,
icon: <Wifi className="h-3 w-3" />,
labelKey: 'status.background',
colorClass: 'text-feedback-success',
animateClass: '',
},
suspended: {
icon: <WifiOff className="w-3 h-3" />,
icon: <WifiOff className="h-3 w-3" />,
labelKey: 'status.suspended',
colorClass: 'text-feedback-warning',
animateClass: '',
},
reconnecting: {
icon: <Loader2 className="w-3 h-3 animate-spin" />,
icon: <Loader2 className="h-3 w-3 animate-spin" />,
labelKey: 'status.reconnecting',
colorClass: 'text-feedback-warning',
animateClass: 'animate-pulse',
},
error: {
icon: <AlertCircle className="w-3 h-3" />,
icon: <AlertCircle className="h-3 w-3" />,
labelKey: 'status.error',
colorClass: 'text-feedback-error',
animateClass: '',
},
disconnected: {
icon: <WifiOff className="w-3 h-3" />,
icon: <WifiOff className="h-3 w-3" />,
labelKey: 'status.disconnected',
colorClass: 'text-txt-muted',
animateClass: '',
@@ -276,8 +275,7 @@ function RemoteHostMetricsStrip({
metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null,
transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null,
...rankedDisks.map(
(d) =>
`${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})`
(d) => `${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})`
),
].filter(Boolean)
@@ -333,21 +331,19 @@ function RemoteHostMetricsStrip({
title={tipParts.join(' · ')}
>
{osLabel && (
<span className="text-content-secondary max-w-[10rem] shrink truncate" title={osLabel}>
<span className="max-w-[10rem] shrink truncate text-content-secondary" title={osLabel}>
{osLabel}
</span>
)}
{tz && <span className="text-txt-faint shrink-0">{tz}</span>}
{tz && <span className="shrink-0 text-txt-faint">{tz}</span>}
{metrics?.cpuPercent != null && (
<span className={`shrink-0 ${pctClass(metrics.cpuPercent)}`}>
CPU {formatCpuPercent(metrics.cpuPercent)}
</span>
<span className={`shrink-0 ${pctClass(metrics.cpuPercent)}`}>CPU {formatCpuPercent(metrics.cpuPercent)}</span>
)}
{metrics?.memPercent != null && (
<span className={`shrink-0 ${pctClass(metrics.memPercent)}`}>MEM {metrics.memPercent}%</span>
)}
{transferLabel && (
<span className="text-txt-muted flex shrink-0 items-center gap-1">
<span className="flex shrink-0 items-center gap-1 text-txt-muted">
<HardDrive className="h-3 w-3" />
{transferLabel}
</span>
@@ -385,8 +381,7 @@ export default function StatusBar() {
const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId])
const { connectionState, hostLabel, stats, connectionId, isRemoteSsh, sessionOsPretty } =
useMemo(() => {
const { connectionState, hostLabel, stats, connectionId, isRemoteSsh, sessionOsPretty } = useMemo(() => {
if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') {
return {
connectionState: undefined,
@@ -425,9 +420,7 @@ export default function StatusBar() {
}
}
const session = activeTab.sessionId
? sessions.find((s) => s.id === activeTab.sessionId)
: undefined
const session = activeTab.sessionId ? sessions.find((s) => s.id === activeTab.sessionId) : undefined
const sessionOsPretty = session?.osPrettyName
const instance = activeTab.connectionInstance
@@ -458,9 +451,7 @@ export default function StatusBar() {
const state = mapTabStatusToConnectionState(activeTab.connectionStatus)
const config = activeTab.connectionConfig
const label = config
? `${config.username}@${config.host}:${config.port || 22}`
: undefined
const label = config ? `${config.username}@${config.host}:${config.port || 22}` : undefined
return {
connectionState: state,
@@ -473,9 +464,7 @@ export default function StatusBar() {
}, [activeTab, connections, sessions, t])
const metricsEnabled =
isRemoteSsh &&
Boolean(connectionId) &&
(connectionState === 'connected' || connectionState === 'background')
isRemoteSsh && Boolean(connectionId) && (connectionState === 'connected' || connectionState === 'background')
const socksAvailable = metricsEnabled
const { metrics, stale } = useRemoteHostMetrics(connectionId, metricsEnabled)
@@ -632,38 +621,11 @@ export default function StatusBar() {
const startForwardRule = useCallback(
async (rule: ForwardRule): Promise<boolean> => {
if (!connectionId || !window.opsxApi) return false
let result
if (rule.type === 'local') {
result = await window.opsxApi.sshForwardLocal(
connectionId,
rule.localPort,
resolveForwardTargetHost(rule.remoteHost),
rule.remotePort || 0,
rule.id,
resolveLocalBindHost(rule.localBindHost)
)
} else if (rule.type === 'remote') {
result = await window.opsxApi.sshForwardRemote(
connectionId,
rule.remotePort || 0,
resolveForwardTargetHost(rule.remoteHost),
rule.localPort,
rule.id,
resolveRemoteBindHost(rule.remoteBindHost)
)
} else {
result = await window.opsxApi.sshForwardDynamic(
connectionId,
resolveDynamicPort(rule.localPort),
rule.id
)
}
if (!result?.success) {
if (!connectionId) return false
const result = await startNativeForward(connectionId, rule)
if (!result.success) {
setForwardDrawerError(
result?.error
? translateErrorMessage(result.error)
: t('common:portForward.errorStartFailed')
translateIpcError(result.error, result.errorCode) || t('common:portForward.errorStartFailed')
)
return false
}
@@ -695,21 +657,10 @@ export default function StatusBar() {
const stopForwardRule = useCallback(
async (rule: ForwardRule): Promise<boolean> => {
if (!window.opsxApi?.sshForwardStop) return false
let result = await window.opsxApi.sshForwardStop(rule.id)
if (!result?.success && connectionId) {
// Runtime id may differ from persisted rule id after reconnect.
const listed = await window.opsxApi.sshForwardList(connectionId)
const match = listed?.tunnels?.find((tunnel) => tunnelCoversRule([tunnel], rule))
if (match) {
result = await window.opsxApi.sshForwardStop(match.forwardId)
}
}
if (!result?.success) {
const result = await stopNativeForward(connectionId, rule)
if (!result.success) {
setForwardDrawerError(
result?.error
? translateErrorMessage(result.error)
: t('common:portForward.errorStopFailed')
translateIpcError(result.error, result.errorCode) || t('common:portForward.errorStopFailed')
)
return false
}
@@ -767,16 +718,7 @@ export default function StatusBar() {
setForwardSaving(false)
}
},
[
connectionId,
forwardRules,
forwardRuntimes,
forwardSaving,
persistForwardRules,
startForwardRule,
t,
toast,
]
[connectionId, forwardRules, forwardRuntimes, forwardSaving, persistForwardRules, startForwardRule, t, toast]
)
const handleStopRule = useCallback(
@@ -788,8 +730,7 @@ export default function StatusBar() {
await stopForwardRule(rule)
} catch (error) {
setForwardDrawerError(
(error instanceof Error ? error.message : String(error)) ||
t('common:portForward.errorStopFailed')
(error instanceof Error ? error.message : String(error)) || t('common:portForward.errorStopFailed')
)
} finally {
setForwardSaving(false)
@@ -811,8 +752,7 @@ export default function StatusBar() {
await persistForwardRules(forwardRules.filter((item) => item.id !== rule.id))
} catch (error) {
setForwardDrawerError(
(error instanceof Error ? error.message : String(error)) ||
t('common:portForward.errorStopFailed')
(error instanceof Error ? error.message : String(error)) || t('common:portForward.errorStopFailed')
)
} finally {
setForwardSaving(false)
@@ -872,14 +812,8 @@ export default function StatusBar() {
type: forwardDraft.type || 'local',
name: forwardDraft.name.trim(),
localPort: forwardDraft.localPort,
localBindHost:
forwardDraft.type === 'local'
? resolveLocalBindHost(forwardDraft.localBindHost)
: undefined,
remoteBindHost:
forwardDraft.type === 'remote'
? resolveRemoteBindHost(forwardDraft.remoteBindHost)
: undefined,
localBindHost: forwardDraft.type === 'local' ? resolveLocalBindHost(forwardDraft.localBindHost) : undefined,
remoteBindHost: forwardDraft.type === 'remote' ? resolveRemoteBindHost(forwardDraft.remoteBindHost) : undefined,
remoteHost: resolveForwardTargetHost(forwardDraft.remoteHost),
remotePort: forwardDraft.remotePort,
}
@@ -959,35 +893,32 @@ export default function StatusBar() {
const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected
const statusLabel = t(statusCfg.labelKey)
const transferLabel =
stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null
const transferLabel = stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null
return (
<div className="border-dark-700/40 bg-dark-800/95 flex h-7 select-none items-center px-3 text-[11px] text-content-primary">
<div className="app-statusbar border-dark-700/40 bg-dark-800/95 flex h-7 select-none items-center px-3 text-[11px] text-content-primary">
<button
type="button"
onClick={togglePanel}
className={`-ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors
hover:bg-dark-700/70 group
${statusCfg.colorClass} ${statusCfg.animateClass}`}
className={`hover:bg-dark-700/70 group -ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors ${statusCfg.colorClass} ${statusCfg.animateClass}`}
title={t('common:connectionPanel.title')}
>
{statusCfg.icon && <span className="transition-colors duration-300">{statusCfg.icon}</span>}
<span className="font-medium transition-colors duration-300">{statusLabel}</span>
{hostLabel && (
<>
<span className="text-txt-faint mx-0.5">·</span>
<span className="text-content-secondary max-w-[280px] truncate font-mono" title={hostLabel}>
<span className="mx-0.5 text-txt-faint">·</span>
<span className="max-w-[280px] truncate font-mono text-content-secondary" title={hostLabel}>
{hostLabel}
</span>
</>
)}
{totalConnections > 0 && (
<span className="bg-accent-blue/20 ml-1 rounded-full px-1.5 py-px text-[10px] font-medium tabular-nums text-accent-blue">
<span className="ml-1 rounded-full bg-accent-blue/20 px-1.5 py-px text-[10px] font-medium tabular-nums text-accent-blue">
{totalConnections}
</span>
)}
<ChevronUp className="text-txt-faint h-3 w-3 transition-transform duration-200 group-hover:rotate-180" />
<ChevronUp className="h-3 w-3 text-txt-faint transition-transform duration-200 group-hover:rotate-180" />
</button>
{metricsEnabled && (
@@ -1014,12 +945,11 @@ export default function StatusBar() {
}
aria-label={t('common:statusBar.forwardOpenIdle', { port: dynamicListenPort })}
aria-pressed={socksRunning}
className={`mr-2 flex shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors disabled:opacity-50
${
socksRunning
? 'bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30'
: 'text-txt-muted hover:bg-dark-700/70 hover:text-content-secondary'
}`}
className={`mr-2 flex shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors disabled:opacity-50 ${
socksRunning
? 'bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30'
: 'hover:bg-dark-700/70 text-txt-muted hover:text-content-secondary'
}`}
>
{forwardSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Globe className="h-3.5 w-3.5" />}
<span className="tabular-nums">
@@ -1028,7 +958,7 @@ export default function StatusBar() {
</button>
)}
<div className="text-txt-faint flex items-center gap-1 tabular-nums">
<div className="flex items-center gap-1 tabular-nums text-txt-faint">
<Clock className="h-3 w-3" />
<span>{formatTime(currentTime)}</span>
</div>
@@ -1046,18 +976,9 @@ export default function StatusBar() {
setForwardDraft({
type: next.type || 'local',
name: next.name || '',
localPort:
next.type === 'dynamic'
? resolveDynamicPort(next.localPort)
: next.localPort || 0,
localBindHost:
next.type === 'local'
? resolveLocalBindHost(next.localBindHost)
: '',
remoteBindHost:
next.type === 'remote'
? resolveRemoteBindHost(next.remoteBindHost)
: '',
localPort: next.type === 'dynamic' ? resolveDynamicPort(next.localPort) : next.localPort || 0,
localBindHost: next.type === 'local' ? resolveLocalBindHost(next.localBindHost) : '',
remoteBindHost: next.type === 'remote' ? resolveRemoteBindHost(next.remoteBindHost) : '',
remoteHost: next.remoteHost || '',
remotePort: next.remotePort || 0,
})