@@ -1,4 +1,4 @@
import { MoonStar , PackageOpen , Pencil , ShieldCheck , Sparkles , Square , UserRoundMinus , UserRoundPlus , WandSparkles } from "lucide-react" ;
import { MoonStar , PackageOpen , Pencil , ShieldCheck , Sparkles , Square , Terminal , UserRoundMinus , UserRoundPlus , WandSparkles } from "lucide-react" ;
import { type FormEvent , useCallback , useEffect , useMemo , useState } from "react" ;
import { platformApiClient } from "../api/client" ;
@@ -6,6 +6,7 @@ import type {
ConfigDiffLineResponse ,
GamePluginResponse ,
JobResponse ,
RemoteAdapterDeclarationResponse ,
ServerInstanceResponse ,
ServerMemberResponse ,
ServerMetricsResponse ,
@@ -13,7 +14,7 @@ import type {
ServerConfigDiffPreviewResponse ,
RunEndpointResponse
} from "../api/types" ;
import { ConfirmDialog , UsageMeter } from "../components/OperationControls" ;
import { ConfirmDialog , ManagementDialog , UsageMeter } from "../components/OperationControls" ;
import { EmptyState , ErrorState , LoadingState , ResultBadge } from "../components/StateViews" ;
import type { PageComponentProps } from "../contracts/page" ;
import { canStartServer , canStopServer , runtimeObservationFreshness , serverMetadataFormFromInstance , type ServerMetadataFormState } from "../contracts/serverManagement" ;
@@ -49,6 +50,8 @@ export function ServerDetailPage(props: PageComponentProps) {
const [ deployment , setDeployment ] = useState < LoadState < ServerDeploymentResponse > > ( { status : "loading" } ) ;
const [ confirm , setConfirm ] = useState < null | { title : string ; description : string ; danger ? : boolean ; run : ( ) = > Promise < void > } > ( null ) ;
const [ confirmBusy , setConfirmBusy ] = useState ( false ) ;
const [ terminalOpen , setTerminalOpen ] = useState ( false ) ;
const [ terminalAdapters , setTerminalAdapters ] = useState < LoadState < RemoteAdapterDeclarationResponse [ ] > > ( { status : "loading" } ) ;
const refresh = useCallback ( async ( ) = > {
if ( ! serverId ) {
@@ -157,6 +160,15 @@ export function ServerDetailPage(props: PageComponentProps) {
} ) ;
}
function openTerminalEntry ( current : ServerInstanceResponse ) {
setTerminalOpen ( true ) ;
setTerminalAdapters ( { status : "loading" } ) ;
void platformApiClient
. listRemoteAdapters ( current . id )
. then ( ( response ) = > setTerminalAdapters ( { status : "ready" , data : response.items } ) )
. catch ( ( error ) = > setTerminalAdapters ( { status : "error" , reason : error instanceof Error ? error . message : "终端能力加载失败" } ) ) ;
}
if ( ! serverId ) {
return (
< EmptyState title = "未选择服务器" description = "请从服务器列表进入详情页。" actionLabel = "返回服务器列表" onAction = { ( ) = > onNavigate ( "servers" ) } / >
@@ -209,6 +221,16 @@ export function ServerDetailPage(props: PageComponentProps) {
< / div >
< div className = "action-strip" >
< span className = { cx ( "status-pill" , detailFreshness === "fresh" ? statusClass ( instance . data . state ) : "status-disabled" ) } > { detailStateText } < / span >
< button
type = "button"
className = "icon-command"
disabled = { ! canManageServers }
title = { canManageServers ? "打开终端" : "当前账号没有管理权限" }
onClick = { ( ) = > openTerminalEntry ( instance . data ) }
>
< Terminal size = { 15 } / >
< span > 打 开 终 端 < / span >
< / button >
< button
type = "button"
className = "icon-command"
@@ -291,10 +313,67 @@ export function ServerDetailPage(props: PageComponentProps) {
} ) ;
} }
/ >
{ instance . status === "ready" && (
< RemoteTerminalEntryDialog
open = { terminalOpen }
instance = { instance . data }
adapters = { terminalAdapters }
onRefresh = { ( ) = > openTerminalEntry ( instance . data ) }
onClose = { ( ) = > setTerminalOpen ( false ) }
/ >
) }
< / section >
) ;
}
interface RemoteTerminalEntryDialogProps {
open : boolean ;
instance : ServerInstanceResponse ;
adapters : LoadState < RemoteAdapterDeclarationResponse [ ] > ;
onRefresh : ( ) = > void ;
onClose : ( ) = > void ;
}
function RemoteTerminalEntryDialog ( { open , instance , adapters , onRefresh , onClose } : RemoteTerminalEntryDialogProps ) {
const terminalReadyAdapters = adapters . status === "ready" ? adapters . data . filter ( isTerminalReadyAdapter ) : [ ] ;
return (
< ManagementDialog
open = { open }
title = "打开终端"
description = { ` 服务器 ${ instance . name } 的快捷入口已保留;这里仅使用 Run 声明的安全远程能力,不向浏览器暴露主机连接细节。 ` }
wide
onClose = { onClose }
>
< div className = "console-row-list" >
< div className = "console-row" > < span > 服 务 器 < / span > < strong > { instance . id } < / strong > < / div >
< div className = "console-row" > < span > Run 节 点 < / span > < strong > { instance . runEndpointId || "未绑定" } < / strong > < / div >
< div className = "console-row" > < span > 终 端 会 话 < / span > < strong > { terminalReadyAdapters . length > 0 ? "等待 Run 会话实现" : "尚未声明交互会话" } < / strong > < / div >
< / div >
< p className = "section-copy" > 直 接 命 令 面 板 已 拆 掉 ; 真 正 的 交 互 终 端 要 由 Run 返 回 受 控 会 话 , 再 由 平 台 打 开 。 当 前 先 展 示 已 声 明 的 适 配 能 力 , 避 免 凭 空 创 建 连 接 或 命 令 。 < / p >
< div className = "action-strip" style = { { marginTop : 12 } } >
< button type = "button" className = "icon-command" onClick = { onRefresh } > < Sparkles size = { 14 } / > < span > 刷 新 能 力 < / span > < / button >
< / div >
{ adapters . status === "loading" && < LoadingState label = "正在读取 Run 远程能力…" compact / > }
{ adapters . status === "error" && < ErrorState title = "终端能力不可用" reason = { adapters . reason } diagnosticId = { ` remote-terminal: ${ instance . id } ` } onRetry = { onRefresh } compact / > }
{ adapters . status === "ready" && adapters . data . length === 0 && < EmptyState title = "暂无远程适配能力" description = "Run 还没有为这个服务器上报可用的安全远程入口。" / > }
{ adapters . status === "ready" && adapters . data . length > 0 && (
< div className = "console-row-list" style = { { marginTop : 12 } } >
{ adapters . data . map ( ( adapter ) = > (
< div key = { adapter . key } className = "console-row" >
< span > { adapter . key } · { adapter . kind } < / span >
< strong > { adapter . capabilities . join ( " / " ) } < / strong >
< / div >
) ) }
< / div >
) }
< / ManagementDialog >
) ;
}
function isTerminalReadyAdapter ( adapter : RemoteAdapterDeclarationResponse ) : boolean {
return adapter . kind === "run-process" || adapter . capabilities . some ( ( capability ) = > capability === "remote.run.process.start" || capability === "remote.run.process.stop" || capability === "remote.run.program.command" ) ;
}
function serverDetailSectionEntries ( plugin? : GamePluginResponse ) : Array < { id : ServerDetailSection ; label : string } > {
const pluginPages = ( plugin ? . pages ? ? [ ] ) . map ( ( page ) = > ( { id : ` plugin: ${ page . key } ` as ServerDetailSection , label : page.title } ) ) ;
return [ . . . pluginPages , . . . serverDetailSections ] ;