@@ -6,18 +6,24 @@ import type {
ConfigDiffLineResponse ,
ArtifactDownloadReferenceResponse ,
ArtifactResponse ,
BackupResponse ,
ClientManagerDistributionResponse ,
DependencyCatalogResponse ,
GamePluginResponse ,
JobResponse ,
LogEntryBody ,
LogStreamResponse ,
RunDistributionResponse ,
RunUpdateJobResponse ,
ServerConfigDiffPreviewResponse ,
ServerConfigResponse ,
ServerInstanceResponse ,
ServerMemberResponse ,
ServerMetricsResponse ,
ServerRuntimeActionsResponse
RuntimeBindingResponse ,
ServerRuntimeActionsResponse ,
MetricSampleResponse ,
RemoteAdapterDeclarationResponse
} from "../api/types" ;
import { ConfirmDialog , DiffView , UsageMeter } from "../components/OperationControls" ;
import {
@@ -34,10 +40,11 @@ import {
import { DiagnosticSummary , EmptyState , ErrorState , LoadingState , ResultBadge } from "../components/StateViews" ;
import type { PageComponentProps } from "../contracts/page" ;
import type { PluginBridgeAction , PluginBridgeManifestContract } from "../contracts/pluginBridge" ;
import { canArchiveServer , canStartServer , canStopServer , pluginLabel , serverMetadataFormFromInstance , type ServerMetadataFormState } from "../contracts/serverManagement" ;
import { canArchiveServer , canStartServer , canStopServer , pluginLabel , runtimeBindingFields , serverMetadataFormFromInstance , type ServerMetadataFormState } from "../contracts/serverManagement" ;
import {
serverDetailSections ,
serverIsOnline ,
isPlatformAdmin ,
type ConfigDiffView ,
type LlmSuggestionView ,
type PluginControlDescriptor ,
@@ -72,7 +79,11 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
const [ plugins , setPlugins ] = useState < GamePluginResponse [ ] > ( [ ] ) ;
const [ jobs , setJobs ] = useState < JobResponse [ ] > ( [ ] ) ;
const [ artifacts , setArtifacts ] = useState < ArtifactResponse [ ] > ( [ ] ) ;
const [ metricHistory , setMetricHistory ] = useState < MetricSampleResponse [ ] > ( [ ] ) ;
const [ backups , setBackups ] = useState < BackupResponse [ ] > ( [ ] ) ;
const [ remoteAdapters , setRemoteAdapters ] = useState < RemoteAdapterDeclarationResponse [ ] > ( [ ] ) ;
const [ runtimeActions , setRuntimeActions ] = useState < LoadState < ServerRuntimeActionsResponse > > ( { status : "loading" } ) ;
const [ runtimeBinding , setRuntimeBinding ] = useState < LoadState < RuntimeBindingResponse > > ( { status : "loading" } ) ;
const [ confirm , setConfirm ] = useState < null | { title : string ; description : string ; danger ? : boolean ; run : ( ) = > Promise < void > } > ( null ) ;
const [ confirmBusy , setConfirmBusy ] = useState ( false ) ;
@@ -83,19 +94,30 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
}
setInstance ( { status : "loading" } ) ;
try {
const [ detail , pluginResponse , jobResponse , runtimeResponse ] = await Promise . all ( [
const [ detail , pluginResponse , jobResponse , runtimeResponse , bindingResponse , metricHistoryResponse , backupResponse , adapterResponse ] = await Promise . all ( [
platformApiClient . getServerInstance ( serverId ) ,
platformApiClient . listGamePlugins ( ) ,
platformApiClient . listJobs ( serverId ) ,
platformApiClient
. getServerRuntimeActions ( serverId )
. then ( ( data ) : LoadState < ServerRuntimeActionsResponse > = > ( { status : "ready" , data } ) )
. catch ( ( error ) : LoadState < ServerRuntimeActionsResponse > = > ( { status : "error" , reason : error instanceof Error ? error . message : "运行分发状态加载失败" } ) )
. catch ( ( error ) : LoadState < ServerRuntimeActionsResponse > = > ( { status : "error" , reason : error instanceof Error ? error . message : "运行分发状态加载失败" } ) ) ,
platformApiClient
. getServerRuntimeBinding ( serverId )
. then ( ( data ) : LoadState < RuntimeBindingResponse > = > ( { status : "ready" , data } ) )
. catch ( ( error ) : LoadState < RuntimeBindingResponse > = > ( { status : "error" , reason : error instanceof Error ? error . message : "运行配置加载失败" } ) ) ,
platformApiClient . listMetricHistory ( serverId ) . catch ( ( ) = > ( { items : [ ] , count : 0 } ) ) ,
platformApiClient . listBackups ( serverId ) . catch ( ( ) = > ( { items : [ ] , count : 0 } ) ) ,
platformApiClient . listRemoteAdapters ( serverId ) . catch ( ( ) = > ( { items : [ ] , count : 0 } ) )
] ) ;
setInstance ( { status : "ready" , data : detail } ) ;
setPlugins ( pluginResponse . items ) ;
setJobs ( jobResponse . items ) ;
setRuntimeActions ( runtimeResponse ) ;
setRuntimeBinding ( bindingResponse ) ;
setMetricHistory ( metricHistoryResponse . items ) ;
setBackups ( backupResponse . items ) ;
setRemoteAdapters ( adapterResponse . items ) ;
const artifactLists = await Promise . all (
jobResponse . items . slice ( 0 , 20 ) . map ( ( job ) = >
platformApiClient
@@ -109,6 +131,10 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
setInstance ( { status : "error" , reason : error instanceof Error ? error . message : "加载失败" } ) ;
setArtifacts ( [ ] ) ;
setRuntimeActions ( { status : "error" , reason : "运行分发状态加载失败" } ) ;
setRuntimeBinding ( { status : "error" , reason : "运行配置加载失败" } ) ;
setMetricHistory ( [ ] ) ;
setBackups ( [ ] ) ;
setRemoteAdapters ( [ ] ) ;
}
try {
const metricsResponse = await platformApiClient . listServerMetrics ( ) ;
@@ -211,7 +237,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
< button
type = "button"
className = "icon-command"
disabled = { ! canStartServer ( instance . data . state ) || operations . isPending ( instance . data . id , "启动服务器" ) }
disabled = { ! canStartServer ( instance . data . state ) || runtimeBinding . status !== "ready" || runtimeBinding . data . status !== "complete" || operations . isPending ( instance . data . id , "启动服务器" ) }
onClick = { ( ) = > requestLifecycle ( instance . data , "start" ) }
>
< WandSparkles size = { 15 } / >
@@ -220,7 +246,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
< button
type = "button"
className = "icon-command danger-command"
disabled = { ! canStopServer ( instance . data . state ) || operations . isPending ( instance . data . id , "停止服务器" ) }
disabled = { ! canStopServer ( instance . data . state ) || runtimeBinding . status !== "ready" || runtimeBinding . data . status !== "complete" || operations . isPending ( instance . data . id , "停止服务器" ) }
onClick = { ( ) = > requestLifecycle ( instance . data , "stop" ) }
>
< Square size = { 15 } / >
@@ -254,6 +280,16 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
< / nav >
{ section === "overview" && < OverviewSection instance = { instance . data } metrics = { metrics } jobs = { jobs } onOpenLogs = { ( ) = > setSection ( "logs" ) } / > }
{ section === "overview" && (
< RuntimeBindingSection
instance = { instance . data }
plugin = { plugins . find ( ( plugin ) = > plugin . id === instance . data . pluginId ) }
binding = { runtimeBinding }
session = { session }
operations = { operations }
onChanged = { ( ) = > void refresh ( ) }
/ >
) }
{ section === "overview" && (
< RuntimeDistributionSection
instance = { instance . data }
@@ -278,7 +314,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
{ section === "config" && < ConfigSection serverId = { serverId } instance = { instance . data } session = { session } operations = { operations } / > }
{ section === "plugins" && < PluginControlsSection serverId = { serverId } instance = { instance . data } plugins = { plugins } artifacts = { artifacts } session = { session } operations = { operations } / > }
{ section === "llm" && < LlmSection serverId = { serverId } instance = { instance . data } session = { session } operations = { operations } / > }
{ section === "history" && < HistorySection serverId = { serverId } serverOperations = { serverOperations } jobs = { jobs } artifacts = { artifacts } / > }
{ section === "history" && < HistorySection serverId = { serverId } serverOperations = { serverOperations } jobs = { jobs } artifacts = { artifacts } metricHistory = { metricHistory } backups = { backups } remoteAdapters = { remoteAdapters } / > }
< / >
) }
@@ -554,7 +590,7 @@ interface OverviewSectionProps {
}
function OverviewSection ( { instance , metrics , jobs , onOpenLogs } : OverviewSectionProps ) {
const pending = jobs . filter ( ( job ) = > job . state === "queued" || job . state === "accepted" || job . state === "running" ) ;
const pending = jobs . filter ( ( job ) = > job . state === "queued" || job . state === "accepted" || job . state === "running" || job . state === "retrying" ) ;
const failed = jobs . filter ( ( job ) = > job . state === "failed" ) ;
return (
< div className = "overview-two-col" >
@@ -602,6 +638,125 @@ interface RuntimeDistributionSectionProps {
onChanged : ( ) = > void ;
}
interface RuntimeBindingSectionProps {
instance : ServerInstanceResponse ;
plugin? : GamePluginResponse ;
binding : LoadState < RuntimeBindingResponse > ;
session : PageComponentProps [ "session" ] ;
operations : PageComponentProps [ "operations" ] ;
onChanged : ( ) = > void ;
}
function RuntimeBindingSection ( { instance , plugin , binding , session , operations , onChanged } : RuntimeBindingSectionProps ) {
const bindingData = binding . status === "ready" ? binding.data : null ;
const [ profileKey , setProfileKey ] = useState ( bindingData ? . profileKey ? ? plugin ? . runtimeProfiles ? . lifecycleProfiles ? . [ 0 ] ? . key ? ? "" ) ;
const [ values , setValues ] = useState < Record < string , string > > ( { } ) ;
const [ result , setResult ] = useState < { status : "succeeded" | "failed" | "pending" ; label : string } | null > ( null ) ;
const canManage = isPlatformAdmin ( session ) || instance . ownerUserId === session . id ;
const activeExistingBinding = bindingData ? . configured === true && ( instance . state === "installing" || instance . state === "running" ) ;
const fields = runtimeBindingFields ( plugin , profileKey ) ;
useEffect ( ( ) = > {
setProfileKey ( bindingData ? . profileKey ? ? plugin ? . runtimeProfiles ? . lifecycleProfiles ? . [ 0 ] ? . key ? ? "" ) ;
setValues ( { } ) ;
} , [ bindingData ? . profileKey , bindingData ? . updatedAt , plugin ? . id ] ) ;
async function saveBinding ( event : FormEvent < HTMLFormElement > ) {
event . preventDefault ( ) ;
const operationId = operations . begin ( { intent : "更新运行配置" , targetKind : "server" , targetId : ` ${ instance . id } :runtime-binding ` , requester : session.displayName } ) ;
setResult ( { status : "pending" , label : "正在保存运行配置" } ) ;
try {
const updated = await platformApiClient . updateServerRuntimeBinding ( instance . id , {
profileKey ,
bindings : Object.fromEntries ( Object . entries ( values ) . map ( ( [ key , value ] ) = > [ key , value . trim ( ) ] ) . filter ( ( [ , value ] ) = > value !== "" ) )
} ) ;
operations . succeed ( operationId , updated . status === "complete" ? "运行配置已就绪" : "运行配置已保存,仍有缺失项" ) ;
setResult ( { status : "succeeded" , label : updated.status === "complete" ? "运行配置已就绪" : ` 仍缺少: ${ updated . missingKeys . join ( "、" ) } ` } ) ;
setValues ( { } ) ;
onChanged ( ) ;
} catch ( error ) {
const reason = error instanceof Error ? error . message : "运行配置保存失败" ;
operations . fail ( operationId , reason , operationId ) ;
setResult ( { status : "failed" , label : reason } ) ;
}
}
return (
< article className = "console-panel" aria-label = "runtime binding" >
< div className = "panel-header" >
< h2 >
< ShieldCheck size = { 16 } style = { { verticalAlign : "-2px" } } / > 运 行 配 置 绑 定
< / h2 >
{ result && < ResultBadge status = { result . status } label = { result . label } / > }
< / div >
{ binding . status === "loading" && < LoadingState label = "正在加载运行配置…" / > }
{ binding . status === "error" && < ErrorState title = "运行配置加载失败" reason = { binding . reason } diagnosticId = { ` runtime-binding: ${ instance . id } ` } onRetry = { onChanged } / > }
{ bindingData && (
< >
< div className = "server-detail-stat-strip" style = { { marginTop : 12 } } >
< HeaderStat label = "绑定状态" value = { bindingData . status === "complete" ? "完整" : "待补齐" } / >
< HeaderStat label = "运行模式" value = { bindingData . mode || "未选择" } / >
< HeaderStat label = "配置项" value = { ` ${ bindingData . keys . filter ( ( key ) = > key . configured ) . length } / ${ bindingData . keys . length } ` } / >
< / div >
{ bindingData . reason && < p className = "page-status" > { bindingData . reason } < / p > }
{ bindingData . missingKeys . length > 0 && < p className = "page-status" > 缺 少 逻 辑 绑 定 : { bindingData . missingKeys . join ( "、" ) } < / p > }
{ bindingData . keys . length > 0 && (
< div className = "tag-list" aria-label = "runtime binding status" >
{ bindingData . keys . map ( ( key ) = > (
< span key = { key . key } className = { cx ( "status-pill" , key . configured ? "status-active" : "status-disabled" ) } >
{ key . key } · { key . configured ? ( key . secret ? "受保护" : "已配置" ) : "缺失" }
< / span >
) ) }
< / div >
) }
< form className = "provider-form" style = { { marginTop : 12 } } onSubmit = { ( event ) = > void saveBinding ( event ) } >
< label >
运 行 配 置
< select
value = { profileKey }
onChange = { ( event ) = > {
setProfileKey ( event . target . value ) ;
setValues ( { } ) ;
} }
disabled = { ! canManage || activeExistingBinding }
required
>
{ ( plugin ? . runtimeProfiles ? . lifecycleProfiles ? ? [ ] ) . map ( ( profile ) = > (
< option key = { profile . key } value = { profile . key } >
{ profile . key } · { profile . mode }
< / option >
) ) }
< / select >
< / label >
< div className = "form-grid" >
{ fields . map ( ( field ) = > {
const existing = bindingData . keys . find ( ( key ) = > key . key === field . key ) ;
return (
< label key = { field . key } >
{ field . key } { field . required ? "(必填)" : "" }
< input
type = { field . sensitive ? "password" : "text" }
autoComplete = "off"
value = { values [ field . key ] ? ? "" }
onChange = { ( event ) = > setValues ( ( current ) = > ( { . . . current , [ field . key ] : event . target . value } ) ) }
placeholder = { existing ? . configured ? "已配置" : "待配置" }
disabled = { ! canManage || activeExistingBinding }
/ >
< / label >
) ;
} ) }
< / div >
< button type = "submit" className = "primary-command" disabled = { ! canManage || activeExistingBinding || ! profileKey } >
< ShieldCheck size = { 16 } / >
< span > 保 存 运 行 配 置 < / span >
< / button >
< / form >
< / >
) }
< / article >
) ;
}
function RuntimeDistributionSection ( { instance , runtimeActions , session , operations , onOpenLogs , onChanged } : RuntimeDistributionSectionProps ) {
const defaults = runtimeDefaultsForPlugin ( instance . pluginId ) ;
const [ targetOs , setTargetOs ] = useState ( defaults . runOs ) ;
@@ -617,9 +772,44 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
const [ lastClient , setLastClient ] = useState < ClientManagerDistributionResponse | null > ( null ) ;
const [ lastDownload , setLastDownload ] = useState < ArtifactDownloadReferenceResponse | null > ( null ) ;
const [ result , setResult ] = useState < { status : "succeeded" | "failed" | "pending" ; label : string } | null > ( null ) ;
const [ dependencyCatalog , setDependencyCatalog ] = useState < LoadState < DependencyCatalogResponse > > ( { status : "loading" } ) ;
const [ runUpdates , setRunUpdates ] = useState < LoadState < RunUpdateJobResponse [ ] > > ( { status : "loading" } ) ;
const runtimeTask = useRuntimeTaskController ( ) ;
const [ runtimeTaskActions , setRuntimeTaskActions ] = useState < RuntimeTaskDialogAction [ ] > ( [ ] ) ;
const refreshRuntimeProjections = useCallback ( async ( ) = > {
const [ catalog , updates ] = await Promise . all ( [
platformApiClient
. getDependencyCatalog ( instance . id )
. then ( ( data ) : LoadState < DependencyCatalogResponse > = > ( { status : "ready" , data } ) )
. catch ( ( error ) : LoadState < DependencyCatalogResponse > = > ( { status : "error" , reason : error instanceof Error ? error . message : "依赖目录加载失败" } ) ) ,
platformApiClient
. listRunUpdates ( instance . id )
. then ( ( data ) : LoadState < RunUpdateJobResponse [ ] > = > ( { status : "ready" , data : data.items } ) )
. catch ( ( error ) : LoadState < RunUpdateJobResponse [ ] > = > ( { status : "error" , reason : error instanceof Error ? error . message : "Run 更新状态加载失败" } ) )
] ) ;
setDependencyCatalog ( catalog ) ;
setRunUpdates ( updates ) ;
} , [ instance . id ] ) ;
useEffect ( ( ) = > {
void refreshRuntimeProjections ( ) ;
} , [ refreshRuntimeProjections ] ) ;
useEffect ( ( ) = > {
if ( dependencyCatalog . status !== "ready" ) return ;
const selectedProbe = dependencyCatalog . data . probes . find ( ( probe ) = > probe . key === probeKey ) ? ? dependencyCatalog . data . probes [ 0 ] ;
if ( selectedProbe && selectedProbe . key !== probeKey ) setProbeKey ( selectedProbe . key ) ;
const matchingPlan = dependencyCatalog . data . plans . find ( ( plan ) = > plan . key === installPlanKey )
? ? dependencyCatalog . data . plans . find ( ( plan ) = > plan . key === selectedProbe ? . installPlanKey )
? ? dependencyCatalog . data . plans [ 0 ] ;
if ( matchingPlan && matchingPlan . key !== installPlanKey ) setInstallPlanKey ( matchingPlan . key ) ;
} , [ dependencyCatalog , installPlanKey , probeKey ] ) ;
const selectedDependencyProbe = dependencyCatalog . status === "ready" ? dependencyCatalog . data . probes . find ( ( probe ) = > probe . key === probeKey ) : undefined ;
const selectedDependencyPlan = dependencyCatalog . status === "ready" ? dependencyCatalog . data . plans . find ( ( plan ) = > plan . key === installPlanKey ) : undefined ;
const latestRunUpdate = runUpdates . status === "ready" ? runUpdates . data [ 0 ] : undefined ;
const actionByKey = useMemo ( ( ) = > {
if ( runtimeActions . status !== "ready" ) {
return new Map < string , { available : boolean ; reason ? : string } > ( ) ;
@@ -676,6 +866,7 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
setResult ( { status : "succeeded" , label } ) ;
runtimeTask . succeedTask ( label ) ;
taskOptions ? . afterSuccess ? . ( value ) ;
void refreshRuntimeProjections ( ) ;
onChanged ( ) ;
} catch ( error ) {
const reason = error instanceof Error ? error . message : ` ${ intent } 失败 ` ;
@@ -802,11 +993,19 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
< / label >
< label >
依 赖 probe
< input value = { probeKey } onChange = { ( event ) = > setProbeKey ( event . target . value ) } / >
< select value = { probeKey } disabled = { dependencyCatalog . status !== "ready" || dependencyCatalog . data . probes . length === 0 } onChange = { ( event ) = > setProbeKey ( event . target . value ) } >
{ dependencyCatalog . status === "ready" && dependencyCatalog . data . probes . map ( ( probe ) = > (
< option key = { probe . key } value = { probe . key } > { probe . key } · { probe . state } < / option >
) ) }
< / select >
< / label >
< label >
安 装 plan
< input value = { installPlanKey } onChange = { ( event ) = > setInstallPlanKey ( event . target . value ) } / >
< select value = { installPlanKey } disabled = { dependencyCatalog . status !== "ready" || dependencyCatalog . data . plans . length === 0 } onChange = { ( event ) = > setInstallPlanKey ( event . target . value ) } >
{ dependencyCatalog . status === "ready" && dependencyCatalog . data . plans . map ( ( plan ) = > (
< option key = { plan . key } value = { plan . key } > { plan . title } · { plan . targetOs } / { plan . targetArch } < / option >
) ) }
< / select >
< / label >
< label >
日 志 源
@@ -847,7 +1046,7 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
/ >
< RuntimeActionRow
title = "run 下载与更新"
description = { lastDownload ? ` 最近下载引用 ${ lastDownload . artifactId } ` : "下载最新 run 包,或用最近生成/下载的 artifact 推送自更新。" }
description = { latestRunUpdate ? ` 最近更新 ${ latestRunUpdate . targetOs } / ${ latestRunUpdate . targetArch } · ${ runUpdatePhaseLabel ( latestRunUpdate . phase ) } ` : lastDownload ? ` 最近下载引用 ${ lastDownload . artifactId } ` : "下载最新 run 包,或用最近生成/下载的 artifact 推送自更新。" }
disabled = { ! canUse ( "download-run" ) }
reason = { reasonFor ( "download-run" ) }
actionLabel = "下载 run"
@@ -889,7 +1088,20 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
}
)
}
/ >
>
{ latestRunUpdate && (
< div className = "tag-list" aria-label = "latest Run update status" >
< span className = { cx ( "status-pill" , latestRunUpdate . phase === "succeeded" ? "status-active" : latestRunUpdate . phase === "failed" || latestRunUpdate . phase === "rolled-back" ? "status-disabled" : "status-pending" ) } >
phase { latestRunUpdate . phase }
< / span >
< span className = "provider-id" title = { latestRunUpdate . checksum } > checksum { shortChecksum ( latestRunUpdate . checksum ) } < / span >
< span className = "provider-id" > release { latestRunUpdate . targetRelease ? ? "pending" } < / span >
< span className = "provider-id" > rollback { latestRunUpdate . rollback ? "yes" : "no" } < / span >
{ latestRunUpdate . message && < span className = "provider-id" > audit { latestRunUpdate . message } < / span > }
< / div >
) }
{ runUpdates . status === "error" && < ResultBadge status = "failed" label = { runUpdates . reason } / > }
< / RuntimeActionRow >
< RuntimeActionRow
title = "run 密钥"
description = "重置后旧 run 包会失效,必须重新生成并重新部署。"
@@ -965,9 +1177,9 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
/ >
< RuntimeActionRow
title = "依赖"
description = { ` 检查 ${ probeKey } ,安装计划 ${ installPlanKey || "未填写" } ` }
disabled = { ! canUse ( "dependencies-check" ) }
reason = { reasonFor ( "dependencies-check" ) }
description = { dependencyCatalog . status === "ready" ? ` ${ dependencyCatalog . data . pluginId } @ ${ dependencyCatalog . data . pluginVersion } · ${ dependencyCatalog . data . profileKey } · ${ dependencyCatalog . data . targetOs } / ${ dependencyCatalog . data . targetArch } ` : "正在读取 Platform 审核后的依赖目录" }
disabled = { ! canUse ( "dependencies-check" ) || dependencyCatalog . status !== "ready" || ! selectedDependencyProbe }
reason = { dependencyCatalog . status === "error" ? dependencyCatalog.reason : dependencyCatalog.status !== "ready" || ! selectedDependencyProbe ? "依赖目录尚未就绪" : reasonFor ( "dependencies-check" ) }
actionLabel = "依赖检查"
onAction = { ( ) = >
void runOperation (
@@ -982,21 +1194,34 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
)
}
secondaryLabel = "依赖安装"
secondaryDisabled = { ! canUse ( "dependencies-install" ) || ! installPlanKey . trim ( ) }
secondaryReason = { ! installPlanKey . trim ( ) ? "请填写插件声明的 install plan " : reasonFor ( "dependencies-install" ) }
secondaryDisabled = { ! canUse ( "dependencies-install" ) || ! selectedDependencyPlan || selectedDependencyProbe ? . installPlanKey !== selectedDependencyPlan . key }
secondaryReason = { ! selectedDependencyPlan ? "请选择 Platform 返回的审核计划" : selectedDependencyProbe ? . installPlanKey !== selectedDependencyPlan . key ? "所选计划不属于当前 probe " : reasonFor ( "dependencies-install" ) }
onSecondary = { ( ) = >
void runOperation (
"依赖安装" ,
( ) = > platformApiClient . installDependencies ( instance . id , dependencyJobRequest ( instance . id , probeKey , installPlanKey ) ) ,
( ) = > platformApiClient . installDependencies ( instance . id , dependencyJobRequest ( instance . id , probeKey , installPlanKey , selectedDependencyPlan ? . digest ? ? "" ) ) ,
( job ) = > ` 依赖安装任务已排队,job ${ job . id } ` ,
{
description : ` 使用 ${ installPlanKey } 安装计划派发依赖安装任务,并保留 job 追踪 。 ` ,
description : ` 审批 ${ installPlanKey } 的 immutable digest ${ shortChecksum ( selectedDependencyPlan ? . digest ? ? "" ) } 后派发依赖安装任务 。` ,
stages : runtimeDependencyStages ,
executeStageIndex : 2
}
)
}
/ >
>
{ selectedDependencyProbe && (
< div className = "tag-list" aria-label = "dependency status and approved plan" >
< span className = { cx ( "status-pill" , selectedDependencyProbe . state === "present" ? "status-active" : selectedDependencyProbe . state === "failed" ? "status-disabled" : "status-pending" ) } >
{ selectedDependencyProbe . key } · { selectedDependencyProbe . state }
< / span >
< span className = "provider-id" > required { selectedDependencyProbe . required ? "yes" : "no" } < / span >
{ selectedDependencyProbe . evidence && < span className = "provider-id" > evidence { selectedDependencyProbe . evidence } < / span > }
{ selectedDependencyPlan && < span className = "provider-id" title = { selectedDependencyPlan . digest } > digest { shortChecksum ( selectedDependencyPlan . digest ) } < / span > }
{ selectedDependencyPlan && < span className = "provider-id" > steps { selectedDependencyPlan . steps . map ( ( step ) = > ` ${ step . type } : ${ step . packageManager ? ? step . downloadHost ? ? step . targetKey } ` ) . join ( " → " ) } < / span > }
< / div >
) }
{ dependencyCatalog . status === "error" && < ResultBadge status = "failed" label = { dependencyCatalog . reason } / > }
< / RuntimeActionRow >
< RuntimeActionRow
title = "日志"
description = "实时日志来自平台日志 API,历史日志通过 backfill job 返回 cursor/ref。"
@@ -1098,6 +1323,24 @@ function runtimeDefaultsForPlugin(pluginId: string) {
} ;
}
function shortChecksum ( value : string ) : string {
if ( ! value ) return "unavailable" ;
return value . length > 22 ? ` ${ value . slice ( 0 , 22 ) } … ` : value ;
}
function runUpdatePhaseLabel ( phase : RunUpdateJobResponse [ "phase" ] ) : string {
switch ( phase ) {
case "queued" : return "等待下载" ;
case "downloading" : return "分块下载与校验" ;
case "staged" : return "已安全暂存" ;
case "restart-requested" : return "等待重启激活" ;
case "activating" : return "激活与健康确认" ;
case "succeeded" : return "更新成功" ;
case "rolled-back" : return "已回滚" ;
case "failed" : return "更新失败" ;
}
}
function safeRuntimeRef ( ref : string ) : string {
if ( ref . startsWith ( "secret://runtime-keys/" ) || ref . startsWith ( "artifact://" ) ) {
return ref ;
@@ -1333,6 +1576,7 @@ function ConfigSection({ serverId, instance, session, operations }: ConfigSectio
try {
const preview = await platformApiClient . previewServerConfigDiff ( serverId , {
expectedConfigVersion : instance.configVersion ,
expectedChecksum : instance.configChecksum ,
key : defaultConfigKey ,
proposedContent : draft
} ) ;
@@ -1352,6 +1596,7 @@ function ConfigSection({ serverId, instance, session, operations }: ConfigSectio
try {
const dispatch = await platformApiClient . approveServerConfigWrite ( serverId , {
expectedConfigVersion : diff.configVersion ? ? instance . configVersion ,
expectedChecksum : diff.checksum ? ? instance . configChecksum ,
key : diff.key ? ? defaultConfigKey ,
proposedContent : diff.nextContent ,
proposedContentInputRef : diff.proposedContentInputRef ,
@@ -1371,7 +1616,7 @@ function ConfigSection({ serverId, instance, session, operations }: ConfigSectio
< article className = "console-panel" aria-label = "server configuration" >
< div className = "panel-header" >
< h2 > 配 置 < / h2 >
{ config . status === "ready" && < span className = "page-status" > 配 置 版 本 v { instance . configVersion } < / span > }
{ config . status === "ready" && < span className = "page-status" > 配 置 版 本 v { instance . configVersion } { instance . configChecksum ? ` · ${ instance . configChecksum . slice ( 0 , 18 ) } ` : "" } < / span > }
< / div >
{ writeOperation && (
< div style = { { marginBottom : 10 } } >
@@ -1440,10 +1685,10 @@ interface PluginControlsSectionProps {
function controlsForPlugin ( plugin : GamePluginResponse ) : PluginControlDescriptor [ ] {
const controls : PluginControlDescriptor [ ] = [ ] ;
for ( const [ action ] of Object . entries ( plugin . lifecycleActions ) ) {
if ( action === "install" || action === "restart" || action === "status" ) {
if ( action === "install" || action === "restart" ) {
continue ;
}
if ( action !== "start" && action !== "stop" ) {
if ( action !== "start" && action !== "stop" && action !== "status" ) {
continue ;
}
controls . push ( {
@@ -1492,6 +1737,8 @@ function lifecycleControlLabel(action: string): string {
return "启动进程" ;
case "stop" :
return "停止进程" ;
case "status" :
return "查询进程" ;
case "restart" :
return "重启进程" ;
default :
@@ -1538,11 +1785,13 @@ function PluginControlsSection({ serverId, instance, plugins, artifacts, session
requester : session.displayName
} ) ;
try {
if ( control . lifecycleAction === "start" || control . lifecycleAction === "stop" ) {
if ( control . lifecycleAction === "start" || control . lifecycleAction === "stop" || control . lifecycleAction === "status" ) {
const result =
control . lifecycleAction === "start"
? await platformApiClient . startServerInstance ( instance . id , serverLifecycleCommandRequest ( instance , "start" ) )
: await platformApiClient . stopServerInstance ( instance . id , serverLifecycleCommandRequest ( instance , "stop" ) ) ;
: control . lifecycleAction === "stop"
? await platformApiClient . stopServerInstance ( instance . id , serverLifecycleCommandRequest ( instance , "stop" ) )
: await platformApiClient . queryServerProcessStatus ( instance . id , serverLifecycleCommandRequest ( instance , "status" ) ) ;
operations . succeed ( operationId , ` 平台生命周期任务 ${ result . job . id } 已派发( ${ result . job . capability } ) ` , result . job ) ;
return ;
}
@@ -1812,6 +2061,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
const preview = response . suggestedConfig
? await platformApiClient . previewServerConfigDiff ( serverId , {
expectedConfigVersion : instance.configVersion ,
expectedChecksum : instance.configChecksum ,
key : defaultConfigKey ,
proposedContent : response.suggestedConfig
} )
@@ -1837,6 +2087,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
try {
const dispatch = await platformApiClient . approveServerConfigWrite ( serverId , {
expectedConfigVersion : suggestion.diff.configVersion ? ? instance . configVersion ,
expectedChecksum : suggestion.diff.checksum ? ? instance . configChecksum ,
key : suggestion.diff.key ? ? defaultConfigKey ,
proposedContent : suggestion.diff.nextContent ,
proposedContentInputRef : suggestion.diff.proposedContentInputRef ,
@@ -1981,6 +2232,7 @@ export function configDiffViewFromPreview(preview: ServerConfigDiffPreviewRespon
return {
serverInstanceId : preview.serverInstanceId ,
configVersion : preview.configVersion ,
checksum : preview.checksum ,
key : preview.key ,
source : preview.source ,
summary : ` + ${ added } / - ${ removed } 行变更 ` ,
@@ -2002,9 +2254,12 @@ interface HistorySectionProps {
serverOperations : PageComponentProps [ "operations" ] [ "operations" ] ;
jobs : JobResponse [ ] ;
artifacts : ArtifactResponse [ ] ;
metricHistory : MetricSampleResponse [ ] ;
backups : BackupResponse [ ] ;
remoteAdapters : RemoteAdapterDeclarationResponse [ ] ;
}
function HistorySection ( { serverId , serverOperations , jobs , artifacts } : HistorySectionProps ) {
function HistorySection ( { serverId , serverOperations , jobs , artifacts , metricHistory , backups , remoteAdapters } : HistorySectionProps ) {
return (
< div className = "overview-two-col" aria-label = "operation history" >
< article className = "console-panel" >
@@ -2071,15 +2326,49 @@ function HistorySection({ serverId, serverOperations, jobs, artifacts }: History
任 务 < code > { job . id } < / code >
< / span >
< span > 进 度 { job . progress . percent } % < / span >
< span >
尝 试 { job . attempt } / { job . retryPolicy . maxAttempts }
< / span >
{ job . nextAttemptAt && < span > 下 次 尝 试 { new Date ( job . nextAttemptAt ) . toLocaleString ( ) } < / span > }
{ job . lastReconciledAt && < span > 最 近 协 调 { new Date ( job . lastReconciledAt ) . toLocaleString ( ) } < / span > }
< span > { new Date ( job . updatedAt ) . toLocaleString ( ) } < / span >
< / div >
{ job . progress . message && < span className = "provider-id" > { job . progress . message } < / span > }
{ job . cancelReason && < span className = "provider-id" > 取 消 原 因 : { job . cancelReason } < / span > }
{ job . reconcileOutcome && < span className = "provider-id" > 协 调 结 果 : { job . reconcileOutcome } < / span > }
{ job . executionResult && ( job . executionResult . processState || job . executionResult . checksum || job . executionResult . version !== undefined ) && (
< span className = "provider-id" >
执 行 结 果 : { job . executionResult . processState ? ? job . executionResult . kind ? ? "已记录" }
{ job . executionResult . version !== undefined ? ` · v ${ job . executionResult . version } ` : "" }
{ job . executionResult . checksum ? ` · ${ job . executionResult . checksum . slice ( 0 , 18 ) } ` : "" }
{ job . executionResult . sizeBytes !== undefined ? ` · ${ job . executionResult . sizeBytes } B ` : "" }
< / span >
) }
< / div >
) ) }
< / div >
) }
< / article >
< ArtifactDownloadPanel serverId = { serverId } artifacts = { artifacts } / >
< article className = "console-panel" aria-label = "durable observability" >
< div className = "panel-header" >
< h2 > 持 久 化 观 测 < / h2 >
< / div >
< div className = "operation-list" >
< div className = "operation-item" >
< div className = "operation-item-head" > < strong > 指 标 样 本 < / strong > < span className = "status-pill status-active" > { metricHistory . length } 条 < / span > < / div >
< div className = "operation-meta" > < span > 最 新 采 集 { metricHistory . length > 0 ? new Date ( metricHistory [ metricHistory . length - 1 ] . collectedAt ) . toLocaleString ( ) : "暂无" } < / span > < / div >
< / div >
< div className = "operation-item" >
< div className = "operation-item-head" > < strong > 备 份 记 录 < / strong > < span className = "status-pill status-active" > { backups . length } 条 < / span > < / div >
< div className = "operation-meta" > { backups . slice ( 0 , 4 ) . map ( ( backup ) = > < span key = { backup . id } > { backup . id } · { backup . state } · { backup . checksum . slice ( 0 , 18 ) } < / span > ) } < / div >
< / div >
< div className = "operation-item" >
< div className = "operation-item-head" > < strong > 远 端 适 配 器 声 明 < / strong > < span className = "status-pill status-active" > { remoteAdapters . length } 个 < / span > < / div >
< div className = "operation-meta" > { remoteAdapters . slice ( 0 , 4 ) . map ( ( adapter ) = > < span key = { adapter . key } > { adapter . key } · { adapter . kind } · { adapter . targetKeys . join ( ", " ) } < / span > ) } < / div >
< / div >
< / div >
< / article >
< / div >
) ;
}