chore: 保存工作副本——客户端授权新契约(双端点/验签/令牌/周检)与桌面会话可靠性改进
This commit is contained in:
1 parent
f71eef0eba
commit
ae6093c9b3
163 files changed
+4911
-2494
No files matched your search
@@ -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,
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user