Remove run node UI and expire registrations
This commit is contained in:
@@ -82,7 +82,6 @@ describe("first-party console pages", () => {
|
||||
refreshedAt: "2026-07-18T10:00:00Z",
|
||||
data: {
|
||||
instances: [],
|
||||
endpoints: [],
|
||||
jobs: [
|
||||
{
|
||||
id: "job-failed-1",
|
||||
@@ -126,7 +125,7 @@ describe("first-party console pages", () => {
|
||||
{...pageProps()}
|
||||
session={readOnlySession}
|
||||
initialState={{
|
||||
core: { status: "ready", data: { instances: [], endpoints: [], jobs: [] }, refreshedAt: "2026-07-18T10:00:00Z" },
|
||||
core: { status: "ready", data: { instances: [], jobs: [] }, refreshedAt: "2026-07-18T10:00:00Z" },
|
||||
metrics: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" },
|
||||
usage: { status: "error", reason: "unavailable", diagnosticId: "usage" },
|
||||
providers: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" }
|
||||
@@ -374,7 +373,7 @@ describe("first-party console pages", () => {
|
||||
|
||||
expect(html).toContain("系统维护");
|
||||
expect(html).toContain("维护排障入口");
|
||||
expect(html).toContain("节点详情");
|
||||
expect(html).toContain("异常服务器");
|
||||
expect(html).toContain("最近失败任务");
|
||||
expect(html).toContain("最近失败任务");
|
||||
});
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
Bot,
|
||||
CakeSlice,
|
||||
Candy,
|
||||
CircleGauge,
|
||||
Info,
|
||||
MoonStar,
|
||||
RotateCw,
|
||||
ServerCog,
|
||||
Sparkles,
|
||||
Workflow
|
||||
} from "lucide-react";
|
||||
@@ -19,13 +17,12 @@ import type {
|
||||
AiProviderResponse,
|
||||
JobResponse,
|
||||
PlatformResourceUsageResponse,
|
||||
RunEndpointResponse,
|
||||
ServerInstanceResponse,
|
||||
ServerMetricsResponse
|
||||
} from "../api/types";
|
||||
import { UsageMeter } from "../components/OperationControls";
|
||||
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||
import { jobBuckets, moduleFreshnessLabel, summarizeEndpointOperations, type OperationsModuleState } from "../contracts/operationsConsole";
|
||||
import { jobBuckets, moduleFreshnessLabel, type OperationsModuleState } from "../contracts/operationsConsole";
|
||||
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import type { GameTypeDistributionEntry, PlatformOverviewSignal } from "../contracts/workspace";
|
||||
@@ -34,7 +31,6 @@ import { cx } from "../utils/classes";
|
||||
|
||||
interface OverviewData {
|
||||
instances: ServerInstanceResponse[];
|
||||
endpoints: RunEndpointResponse[];
|
||||
jobs: JobResponse[];
|
||||
}
|
||||
|
||||
@@ -58,14 +54,13 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
const refreshCore = useCallback(async () => {
|
||||
setCore({ status: "loading" });
|
||||
try {
|
||||
const [instances, endpoints, jobs] = await Promise.all([
|
||||
const [instances, jobs] = await Promise.all([
|
||||
platformApiClient.listServerInstances(),
|
||||
platformApiClient.listRunEndpoints(),
|
||||
platformApiClient.listJobs()
|
||||
]);
|
||||
setCore({ status: "ready", data: { instances: instances.items, endpoints: endpoints.items, jobs: jobs.items }, refreshedAt: refreshedNow() });
|
||||
setCore({ status: "ready", data: { instances: instances.items, jobs: jobs.items }, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setCore({ status: "error", reason: errorMessage(error, "服务器、节点或任务加载失败"), diagnosticId: "overview-core" });
|
||||
setCore({ status: "error", reason: errorMessage(error, "服务器或任务加载失败"), diagnosticId: "overview-core" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -126,7 +121,6 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
|
||||
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => buildOverviewSignals(core, providers), [core, providers]);
|
||||
const jobs = core.status === "ready" ? jobBuckets(core.data.jobs) : null;
|
||||
const endpointSummary = core.status === "ready" ? summarizeEndpointOperations(core.data.endpoints) : null;
|
||||
const onlineCount = core.status === "ready" ? core.data.instances.filter((item) => serverIsOnline(item.state)).length : 0;
|
||||
const offlineCount = core.status === "ready" ? core.data.instances.length - onlineCount : 0;
|
||||
const activeProviders = providers.status === "ready" ? providers.data.filter((item) => item.status === "active").length : 0;
|
||||
@@ -172,13 +166,13 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{core.status === "loading" && <LoadingState label="正在加载服务器、节点与任务概况…" />}
|
||||
{core.status === "loading" && <LoadingState label="正在加载服务器与任务概况…" />}
|
||||
{core.status === "error" && <ErrorState title="平台核心概况不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} />}
|
||||
{core.status === "ready" && core.data.instances.length === 0 && (
|
||||
<EmptyState
|
||||
icon={<CakeSlice size={26} />}
|
||||
title="还没有服务器实例"
|
||||
description={canManageServers ? "平台尚未创建服务器。先确认运行节点在线,再创建第一个实例。" : "当前账号可查看概览,但没有创建服务器的权限。"}
|
||||
description={canManageServers ? "平台尚未创建服务器。先创建第一个实例,再生成并启动 Run。" : "当前账号可查看概览,但没有创建服务器的权限。"}
|
||||
actionLabel="前往服务器管理"
|
||||
onAction={() => onNavigate("servers")}
|
||||
/>
|
||||
@@ -196,11 +190,6 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
<strong className="metric-value">{offlineCount}</strong>
|
||||
<p>{core.data.instances.filter((item) => item.state === "failed").length} 个异常</p>
|
||||
</article>
|
||||
<article className={cx("overview-card", endpointSummary && endpointSummary.degraded + endpointSummary.offline > 0 ? "metric-tone-warning" : "metric-tone-neutral")}>
|
||||
<span className="metric-label">运行节点</span>
|
||||
<strong className="metric-value">{endpointSummary?.online ?? 0} 在线</strong>
|
||||
<p>{endpointSummary ? `${endpointSummary.degraded} 降级,${endpointSummary.offline} 离线` : "节点状态不可用"}</p>
|
||||
</article>
|
||||
<article className={cx("overview-card", providers.status === "error" || errorProviders > 0 ? "metric-tone-warning" : "metric-tone-success")}>
|
||||
<span className="metric-label">AI 提供商</span>
|
||||
<strong className="metric-value">{providers.status === "ready" ? `${activeProviders} 可用` : providers.status === "loading" ? "加载中" : "不可用"}</strong>
|
||||
@@ -246,36 +235,6 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
)}
|
||||
</article>
|
||||
|
||||
<article className="console-panel console-module" aria-label="运行节点状态">
|
||||
<div className="panel-header">
|
||||
<h2><ServerCog size={16} /> 运行节点</h2>
|
||||
<button type="button" className="icon-command" onClick={() => onNavigate("maintenance")}><CircleGauge size={14} /> 排障</button>
|
||||
</div>
|
||||
{core.status === "loading" && <LoadingState label="正在汇总节点…" compact />}
|
||||
{core.status === "error" && <ErrorState title="节点状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
||||
{core.status === "ready" && endpointSummary && (
|
||||
<>
|
||||
<dl className="console-stat-strip">
|
||||
<div><dt>在线</dt><dd>{endpointSummary.online}</dd></div>
|
||||
<div><dt>活跃任务</dt><dd>{endpointSummary.activeJobs}</dd></div>
|
||||
<div><dt>排队</dt><dd>{endpointSummary.queuedJobs}</dd></div>
|
||||
</dl>
|
||||
{core.data.endpoints.length === 0 ? (
|
||||
<p className="console-empty-note">Platform 未返回运行节点。</p>
|
||||
) : (
|
||||
<div className="console-row-list">
|
||||
{core.data.endpoints.slice(0, 4).map((endpoint) => (
|
||||
<div key={endpoint.id} className="console-row">
|
||||
<span><strong>{endpoint.displayName}</strong><small>{endpoint.version}</small></span>
|
||||
<span className={cx("status-pill", `status-${endpoint.status}`)}>{endpointStatusLabel(endpoint.status)}</span>
|
||||
<span>{endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs} 运行</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</section>
|
||||
<section className="overview-two-col">
|
||||
<article className="console-panel" aria-label="resource usage">
|
||||
@@ -386,8 +345,3 @@ function jobStateLabel(state: JobResponse["state"]): string {
|
||||
const labels: Record<JobResponse["state"], string> = { queued: "排队", accepted: "已领取", running: "执行中", retrying: "等待重试", succeeded: "完成", failed: "失败", cancelled: "已取消" };
|
||||
return labels[state];
|
||||
}
|
||||
|
||||
function endpointStatusLabel(status: RunEndpointResponse["status"]): string {
|
||||
const labels: Record<RunEndpointResponse["status"], string> = { online: "在线", offline: "离线", degraded: "降级", disabled: "停用" };
|
||||
return labels[status];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ListChecks, RotateCcw, ServerCog, Sparkles, WandSparkles } from "lucide-react";
|
||||
import { AlertTriangle, ListChecks, RotateCcw, Sparkles, WandSparkles } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { JobResponse, RunEndpointResponse, ServerInstanceResponse } from "../api/types";
|
||||
import type { JobResponse, ServerInstanceResponse } from "../api/types";
|
||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
@@ -11,21 +11,10 @@ import { cx } from "../utils/classes";
|
||||
type ModuleState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
||||
|
||||
export function MaintenancePage({ session, operations, onNavigate }: PageComponentProps) {
|
||||
const [endpoints, setEndpoints] = useState<ModuleState<RunEndpointResponse[]>>({ status: "loading" });
|
||||
const [jobs, setJobs] = useState<ModuleState<JobResponse[]>>({ status: "loading" });
|
||||
const [servers, setServers] = useState<ModuleState<ServerInstanceResponse[]>>({ status: "loading" });
|
||||
const [triageResult, setTriageResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
|
||||
|
||||
const refreshEndpoints = useCallback(async () => {
|
||||
setEndpoints({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listRunEndpoints();
|
||||
setEndpoints({ status: "ready", data: response.items });
|
||||
} catch (error) {
|
||||
setEndpoints({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshJobs = useCallback(async () => {
|
||||
setJobs({ status: "loading" });
|
||||
try {
|
||||
@@ -47,22 +36,19 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
}, []);
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
void refreshEndpoints();
|
||||
void refreshJobs();
|
||||
void refreshServers();
|
||||
}, [refreshEndpoints, refreshJobs, refreshServers]);
|
||||
}, [refreshJobs, refreshServers]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshAll();
|
||||
}, [refreshAll]);
|
||||
|
||||
const endpointItems = endpoints.status === "ready" ? endpoints.data : [];
|
||||
const jobItems = jobs.status === "ready" ? jobs.data : [];
|
||||
const serverItems = servers.status === "ready" ? servers.data : [];
|
||||
const failedJobs = useMemo(() => jobItems.filter((job) => job.state === "failed").slice(0, 8), [jobItems]);
|
||||
const failedServers = useMemo(() => serverItems.filter((server) => server.state === "failed").slice(0, 8), [serverItems]);
|
||||
const serverById = useMemo(() => new Map(serverItems.map((server) => [server.id, server])), [serverItems]);
|
||||
const endpointById = useMemo(() => new Map(endpointItems.map((endpoint) => [endpoint.id, endpoint])), [endpointItems]);
|
||||
const unhealthyEndpointCount = endpointItems.filter((endpoint) => endpoint.status !== "online" || heartbeatAgeMinutes(endpoint.lastHeartbeatAt) > 5).length;
|
||||
|
||||
async function retryJob(job: JobResponse) {
|
||||
const retryStamp = Date.now();
|
||||
@@ -106,15 +92,15 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
|
||||
<div className="form-guidance maintenance-triage-intro">
|
||||
<strong>维护排障入口</strong>
|
||||
<span>从节点详情和最近失败任务进入重试、查看相关服务器、查看日志链路,不需要直接接触 run 端。</span>
|
||||
<span>从异常服务器和最近失败任务进入重试、查看相关服务器、查看日志链路。</span>
|
||||
</div>
|
||||
|
||||
<section className="maintenance-triage-grid" aria-label="维护排障入口">
|
||||
<button type="button" className="triage-card" onClick={() => void refreshEndpoints()}>
|
||||
<ServerCog size={18} />
|
||||
<span>节点详情</span>
|
||||
<strong>{endpoints.status === "ready" ? `${unhealthyEndpointCount} 个需关注` : "加载中"}</strong>
|
||||
<small>查看心跳、容量、能力标签、相关服务器和日志链路。</small>
|
||||
<button type="button" className="triage-card" onClick={() => void refreshServers()}>
|
||||
<AlertTriangle size={18} />
|
||||
<span>异常服务器</span>
|
||||
<strong>{servers.status === "ready" ? `${failedServers.length} 个需关注` : "加载中"}</strong>
|
||||
<small>查看失败实例、更新时间和关联日志链路。</small>
|
||||
</button>
|
||||
<button type="button" className="triage-card" onClick={() => void refreshJobs()}>
|
||||
<ListChecks size={18} />
|
||||
@@ -125,64 +111,39 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
</section>
|
||||
|
||||
{triageResult && <ResultBadge status={triageResult.status} label={triageResult.label} />}
|
||||
<section className="console-panel" aria-label="run endpoints">
|
||||
|
||||
<section className="console-panel" aria-label="failed servers">
|
||||
<div className="panel-header">
|
||||
<h2>运行节点</h2>
|
||||
<h2>异常服务器</h2>
|
||||
</div>
|
||||
{endpoints.status === "loading" && <LoadingState label="正在加载运行节点…" compact />}
|
||||
{endpoints.status === "error" && (
|
||||
<ErrorState title="运行节点加载失败" reason={endpoints.reason} diagnosticId="maintenance-endpoints" onRetry={() => void refreshEndpoints()} compact />
|
||||
{servers.status === "loading" && <LoadingState label="正在加载服务器…" compact />}
|
||||
{servers.status === "error" && <ErrorState title="服务器加载失败" reason={servers.reason} diagnosticId="maintenance-servers" onRetry={() => void refreshServers()} compact />}
|
||||
{servers.status === "ready" && failedServers.length === 0 && (
|
||||
<EmptyState title="暂无异常服务器" description="当前没有处于失败状态的服务器实例。" actionLabel="刷新服务器" onAction={() => void refreshServers()} />
|
||||
)}
|
||||
{servers.status === "error" && <ErrorState title="相关服务器加载失败" reason={servers.reason} diagnosticId="maintenance-servers" onRetry={() => void refreshServers()} compact />}
|
||||
{endpoints.status === "ready" && endpoints.data.length === 0 && (
|
||||
<EmptyState title="暂无运行节点" description="还没有运行端注册到平台。" actionLabel="刷新" onAction={() => void refreshEndpoints()} />
|
||||
)}
|
||||
{endpoints.status === "ready" && endpoints.data.length > 0 && (
|
||||
<div className="resource-list maintenance-node-list">
|
||||
{endpoints.data.map((endpoint) => {
|
||||
const relatedServers = serverItems.filter((server) => server.runEndpointId === endpoint.id);
|
||||
const firstRelatedServer = relatedServers[0];
|
||||
return (
|
||||
<article key={endpoint.id} className="resource-list-item maintenance-node-item">
|
||||
<div>
|
||||
<strong>{endpoint.displayName}</strong>
|
||||
<span className="provider-id">{endpoint.id}</span>
|
||||
</div>
|
||||
<span className={cx("status-pill", endpointStatusClass(endpoint))}>{endpointStatusLabel(endpoint)}</span>
|
||||
<span>
|
||||
任务 {endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs}(排队 {endpoint.capacity.queuedJobs})
|
||||
</span>
|
||||
<span>{heartbeatReason(endpoint)}</span>
|
||||
<details className="node-detail">
|
||||
<summary>节点详情</summary>
|
||||
<span>版本 {endpoint.version}</span>
|
||||
<span>能力 {endpoint.capabilities.slice(0, 4).join(" / ") || "未上报"}</span>
|
||||
<span>相关服务器 {relatedServers.length}</span>
|
||||
</details>
|
||||
<div className="console-row-actions">
|
||||
<button type="button" className="theme-upload" onClick={() => void refreshEndpoints()}>
|
||||
刷新节点
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="theme-upload"
|
||||
disabled={!firstRelatedServer}
|
||||
onClick={() => firstRelatedServer && onNavigate("serverDetail", { serverId: firstRelatedServer.id })}
|
||||
>
|
||||
查看相关服务器
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="theme-upload"
|
||||
disabled={!firstRelatedServer}
|
||||
onClick={() => firstRelatedServer && onNavigate("serverDetail", { serverId: firstRelatedServer.id })}
|
||||
>
|
||||
查看日志链路
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
{servers.status === "ready" && failedServers.length > 0 && (
|
||||
<div className="console-record-list">
|
||||
{failedServers.map((server) => (
|
||||
<div key={server.id} className="console-record">
|
||||
<div className="console-record-head">
|
||||
<strong>{server.name}</strong>
|
||||
<span className={cx("status-pill", "status-error")}>{serverStateLabel(server.state)}</span>
|
||||
</div>
|
||||
<div className="console-record-meta">
|
||||
<span>服务器 <code>{server.id}</code></span>
|
||||
<span>插件 {server.pluginId}@{server.pluginVersion}</span>
|
||||
<span>{formatTimestamp(server.updatedAt)}</span>
|
||||
</div>
|
||||
<div className="console-row-actions">
|
||||
<button type="button" className="theme-upload" onClick={() => onNavigate("serverDetail", { serverId: server.id })}>
|
||||
查看相关服务器
|
||||
</button>
|
||||
<button type="button" className="theme-upload" onClick={() => onNavigate("serverDetail", { serverId: server.id })}>
|
||||
查看日志链路
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -194,13 +155,12 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
{jobs.status === "loading" && <LoadingState label="正在加载任务…" compact />}
|
||||
{jobs.status === "error" && <ErrorState title="任务加载失败" reason={jobs.reason} diagnosticId="maintenance-jobs" onRetry={() => void refreshJobs()} compact />}
|
||||
{jobs.status === "ready" && failedJobs.length === 0 && (
|
||||
<EmptyState title="暂无失败任务" description="最近任务没有失败记录;如果节点异常,请先查看运行节点心跳和容量。" actionLabel="刷新任务" onAction={() => void refreshJobs()} />
|
||||
<EmptyState title="暂无失败任务" description="最近任务没有失败记录。" actionLabel="刷新任务" onAction={() => void refreshJobs()} />
|
||||
)}
|
||||
{jobs.status === "ready" && failedJobs.length > 0 && (
|
||||
<div className="console-record-list">
|
||||
{failedJobs.map((job) => {
|
||||
const server = job.serverInstanceId ? serverById.get(job.serverInstanceId) : undefined;
|
||||
const endpoint = endpointById.get(job.runEndpointId);
|
||||
return (
|
||||
<div key={job.id} className="console-record">
|
||||
<div className="console-record-head">
|
||||
@@ -208,15 +168,10 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
<span className="status-pill status-error">{jobStateLabel(job.state)}</span>
|
||||
</div>
|
||||
<div className="console-record-meta">
|
||||
<span>
|
||||
任务 <code>{job.id}</code>
|
||||
</span>
|
||||
<span>任务 <code>{job.id}</code></span>
|
||||
<span>服务器 {server ? server.name : job.serverInstanceId ?? "平台任务"}</span>
|
||||
<span>节点 {endpoint ? endpoint.displayName : job.runEndpointId}</span>
|
||||
<span>{progressMessage(job)}</span>
|
||||
<span>
|
||||
尝试 {job.attempt}/{job.retryPolicy.maxAttempts}
|
||||
</span>
|
||||
<span>尝试 {job.attempt}/{job.retryPolicy.maxAttempts}</span>
|
||||
<span>{formatTimestamp(job.updatedAt)}</span>
|
||||
</div>
|
||||
<div className="console-row-actions">
|
||||
@@ -237,53 +192,10 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function endpointStatusClass(endpoint: RunEndpointResponse): string {
|
||||
if (endpoint.status === "online" && heartbeatAgeMinutes(endpoint.lastHeartbeatAt) <= 5) {
|
||||
return "status-active";
|
||||
}
|
||||
if (endpoint.status === "offline") {
|
||||
return "status-error";
|
||||
}
|
||||
return "status-disabled";
|
||||
}
|
||||
|
||||
function endpointStatusLabel(endpoint: RunEndpointResponse): string {
|
||||
if (endpoint.status === "online" && heartbeatAgeMinutes(endpoint.lastHeartbeatAt) > 5) {
|
||||
return "心跳延迟";
|
||||
}
|
||||
return endpoint.status === "online" ? "在线" : endpoint.status === "offline" ? "离线" : endpoint.status;
|
||||
}
|
||||
|
||||
function heartbeatReason(endpoint: RunEndpointResponse): string {
|
||||
const age = heartbeatAgeMinutes(endpoint.lastHeartbeatAt);
|
||||
if (!Number.isFinite(age)) {
|
||||
return `心跳时间异常:${endpoint.lastHeartbeatAt}`;
|
||||
}
|
||||
if (endpoint.status === "offline") {
|
||||
return `心跳异常:节点离线,最后 ${formatTimestamp(endpoint.lastHeartbeatAt)}`;
|
||||
}
|
||||
if (age > 5) {
|
||||
return `心跳异常:${Math.round(age)} 分钟未更新`;
|
||||
}
|
||||
if (endpoint.capacity.runningJobs >= endpoint.capacity.maxJobs) {
|
||||
return "容量已满:等待任务会继续排队";
|
||||
}
|
||||
return `心跳正常:${formatTimestamp(endpoint.lastHeartbeatAt)}`;
|
||||
}
|
||||
|
||||
function heartbeatAgeMinutes(value: string): number {
|
||||
const timestamp = new Date(value).getTime();
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return (Date.now() - timestamp) / 60000;
|
||||
}
|
||||
|
||||
function jobStateLabel(state: JobResponse["state"]): string {
|
||||
switch (state) {
|
||||
case "queued":
|
||||
@@ -303,6 +215,25 @@ function jobStateLabel(state: JobResponse["state"]): string {
|
||||
}
|
||||
}
|
||||
|
||||
function serverStateLabel(state: ServerInstanceResponse["state"]): string {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "运行中";
|
||||
case "stopped":
|
||||
return "已停止";
|
||||
case "installing":
|
||||
return "安装中";
|
||||
case "ready":
|
||||
return "就绪";
|
||||
case "deleted":
|
||||
return "已删除";
|
||||
case "draft":
|
||||
return "草稿";
|
||||
default:
|
||||
return "失败";
|
||||
}
|
||||
}
|
||||
|
||||
function progressMessage(job: JobResponse): string {
|
||||
return job.progress.message ? `原因 ${job.progress.message}` : `进度 ${job.progress.percent}%`;
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
title="暂无可管理的服务器"
|
||||
description={
|
||||
isPlatformAdmin(session)
|
||||
? "平台还没有服务器实例。点击上方“创建服务器”开始,或检查运行节点状态。"
|
||||
? "平台还没有服务器实例。点击上方“创建服务器”开始,再生成并启动 Run。"
|
||||
: "当前账号名下没有可管理的服务器。如果这不符合预期,请联系平台管理员为你分配服务器,或点击刷新重试。"
|
||||
}
|
||||
actionLabel="刷新"
|
||||
|
||||
Reference in New Issue
Block a user