功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+240 -144
View File
@@ -1,4 +1,17 @@
import { Activity, AlertTriangle, CakeSlice, Candy, Info, MoonStar, Sparkles } from "lucide-react";
import {
Activity,
AlertTriangle,
Bot,
CakeSlice,
Candy,
CircleGauge,
Info,
MoonStar,
RotateCw,
ServerCog,
Sparkles,
Workflow
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
@@ -12,26 +25,38 @@ import type {
ServerMetricsResponse
} from "../api/types";
import { UsageMeter } from "../components/OperationControls";
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
import { jobBuckets, moduleFreshnessLabel, summarizeEndpointOperations, type OperationsModuleState } from "../contracts/operationsConsole";
import type { PageComponentProps } from "../contracts/page";
import type { GameTypeDistributionEntry, PlatformOverviewSignal } from "../contracts/workspace";
import { serverIsOnline } from "../contracts/workspace";
import { cx } from "../utils/classes";
type ModuleState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
interface OverviewData {
instances: ServerInstanceResponse[];
endpoints: RunEndpointResponse[];
jobs: JobResponse[];
}
export function HomePage({ onNavigate }: PageComponentProps) {
const [core, setCore] = useState<ModuleState<OverviewData>>({ status: "loading" });
const [metrics, setMetrics] = useState<ModuleState<ServerMetricsResponse[]>>({ status: "loading" });
const [usage, setUsage] = useState<ModuleState<PlatformResourceUsageResponse>>({ status: "loading" });
const [providers, setProviders] = useState<ModuleState<AiProviderResponse[]>>({ status: "loading" });
const [signals, setSignals] = useState<ModuleState<AuditEventResponse[]>>({ status: "loading" });
export interface HomePageInitialState {
core?: OperationsModuleState<OverviewData>;
metrics?: OperationsModuleState<ServerMetricsResponse[]>;
usage?: OperationsModuleState<PlatformResourceUsageResponse>;
providers?: OperationsModuleState<AiProviderResponse[]>;
signals?: OperationsModuleState<AuditEventResponse[]>;
}
interface HomePageProps extends PageComponentProps {
initialState?: HomePageInitialState;
}
export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
const [core, setCore] = useState<OperationsModuleState<OverviewData>>(initialState?.core ?? { status: "loading" });
const [metrics, setMetrics] = useState<OperationsModuleState<ServerMetricsResponse[]>>(initialState?.metrics ?? { status: "loading" });
const [usage, setUsage] = useState<OperationsModuleState<PlatformResourceUsageResponse>>(initialState?.usage ?? { status: "loading" });
const [providers, setProviders] = useState<OperationsModuleState<AiProviderResponse[]>>(initialState?.providers ?? { status: "loading" });
const [signals, setSignals] = useState<OperationsModuleState<AuditEventResponse[]>>(initialState?.signals ?? { status: "loading" });
const refreshCore = useCallback(async () => {
setCore({ status: "loading" });
@@ -41,9 +66,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
platformApiClient.listRunEndpoints(),
platformApiClient.listJobs()
]);
setCore({ status: "ready", data: { instances: instances.items, endpoints: endpoints.items, jobs: jobs.items } });
setCore({ status: "ready", data: { instances: instances.items, endpoints: endpoints.items, jobs: jobs.items }, refreshedAt: refreshedNow() });
} catch (error) {
setCore({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
setCore({ status: "error", reason: errorMessage(error, "服务器、节点或任务加载失败"), diagnosticId: "overview-core" });
}
}, []);
@@ -51,9 +76,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
setMetrics({ status: "loading" });
try {
const response = await platformApiClient.listServerMetrics();
setMetrics({ status: "ready", data: response.items });
} catch {
setMetrics({ status: "ready", data: [] });
setMetrics({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
} catch (error) {
setMetrics({ status: "error", reason: errorMessage(error, "服务器指标加载失败"), diagnosticId: "overview-server-metrics" });
}
}, []);
@@ -61,9 +86,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
setUsage({ status: "loading" });
try {
const response = await platformApiClient.getPlatformResourceUsage();
setUsage({ status: "ready", data: response });
} catch {
setUsage({ status: "error", reason: "平台资源指标接口尚未提供" });
setUsage({ status: "ready", data: response, refreshedAt: refreshedNow() });
} catch (error) {
setUsage({ status: "error", reason: errorMessage(error, "平台资源指标加载失败"), diagnosticId: "overview-platform-usage" });
}
}, []);
@@ -71,9 +96,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
setProviders({ status: "loading" });
try {
const response = await platformApiClient.listAiProviders();
setProviders({ status: "ready", data: response.items });
setProviders({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
} catch (error) {
setProviders({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
setProviders({ status: "error", reason: errorMessage(error, "AI 提供商状态加载失败"), diagnosticId: "overview-ai-providers" });
}
}, []);
@@ -81,19 +106,26 @@ export function HomePage({ onNavigate }: PageComponentProps) {
setSignals({ status: "loading" });
try {
const response = await platformApiClient.listAuditEvents();
setSignals({ status: "ready", data: response.items });
} catch {
setSignals({ status: "ready", data: [] });
setSignals({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
} catch (error) {
setSignals({ status: "error", reason: errorMessage(error, "审计事件加载失败"), diagnosticId: "overview-audit-events" });
}
}, []);
useEffect(() => {
const refreshAll = useCallback(() => {
void refreshCore();
void refreshMetrics();
void refreshUsage();
void refreshProviders();
void refreshSignals();
}, [refreshCore, refreshMetrics, refreshUsage, refreshProviders, refreshSignals]);
}, [refreshCore, refreshMetrics, refreshProviders, refreshSignals, refreshUsage]);
useEffect(() => {
if (initialState) {
return;
}
refreshAll();
}, [initialState, refreshAll]);
const distribution = useMemo<GameTypeDistributionEntry[]>(() => {
if (core.status !== "ready") {
@@ -106,73 +138,19 @@ export function HomePage({ onNavigate }: PageComponentProps) {
return [...counts.entries()].map(([serverType, count]) => ({ serverType, label: serverType, count }));
}, [core]);
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => {
const collected: PlatformOverviewSignal[] = [];
if (core.status === "ready") {
for (const instance of core.data.instances.filter((item) => item.state === "failed")) {
collected.push({
id: `fault-${instance.id}`,
kind: "fault",
summary: `服务器 ${instance.name} 处于异常状态`,
detail: `实例 ${instance.id} 状态为 failed,需要排查`,
targetPage: "servers",
targetId: instance.id,
tone: "error",
at: instance.updatedAt
});
}
for (const job of core.data.jobs.filter((item) => item.state === "failed").slice(0, 5)) {
collected.push({
id: `job-${job.id}`,
kind: "job",
summary: `任务 ${job.capability} 失败`,
detail: `任务 ${job.id}${job.serverInstanceId ? `(服务器 ${job.serverInstanceId}` : ""} 执行失败`,
targetPage: "servers",
targetId: job.serverInstanceId,
tone: "error",
at: job.updatedAt
});
}
}
if (providers.status === "ready") {
for (const provider of providers.data.filter((item) => item.status === "error")) {
collected.push({
id: `ai-${provider.id}`,
kind: "aiProvider",
summary: `AI 提供商 ${provider.name} 连接异常`,
detail: `提供商 ${provider.id} 状态为 error,LLM 辅助可能不可用`,
targetPage: "aiProviders",
targetId: provider.id,
tone: "warning",
at: ""
});
}
}
if (signals.status === "ready") {
for (const event of signals.data.slice(0, 5)) {
collected.push({
id: `audit-${event.id}`,
kind: "log",
summary: event.summary || `${event.action} ${event.resourceKind}`,
detail: `${event.actorId}${event.resourceKind}/${event.resourceId} 执行 ${event.action}${event.result}`,
targetPage: event.resourceKind === "server-instance" ? "servers" : "maintenance",
targetId: event.resourceId,
tone: event.result === "failure" ? "warning" : "info",
at: event.createdAt
});
}
}
return collected.slice(0, 8);
}, [core, providers, signals]);
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => buildOverviewSignals(core, providers, signals), [core, providers, signals]);
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;
const errorProviders = providers.status === "ready" ? providers.data.filter((item) => item.status === "error").length : 0;
const moduleFailureCount = [core, metrics, usage, providers, signals].filter((module) => module.status === "error").length;
const canManageServers = session.capabilities.includes("servers.manage");
const metricAverages = useMemo(() => {
if (usage.status === "ready") {
return { cpu: usage.data.cpuPercent, memory: usage.data.memoryPercent, disk: usage.data.diskPercent, source: "平台指标" };
return { cpu: usage.data.cpuPercent, memory: usage.data.memoryPercent, disk: usage.data.diskPercent, source: "平台指标", refreshedAt: usage.refreshedAt };
}
if (metrics.status === "ready" && metrics.data.length > 0) {
const average = (values: Array<number | undefined>) => {
@@ -183,38 +161,45 @@ export function HomePage({ onNavigate }: PageComponentProps) {
cpu: average(metrics.data.map((item) => item.cpuPercent)),
memory: average(metrics.data.map((item) => item.memoryPercent)),
disk: average(metrics.data.map((item) => item.diskPercent)),
source: "服务器均值"
source: "服务器指标均值",
refreshedAt: metrics.refreshedAt
};
}
return { cpu: undefined, memory: undefined, disk: undefined, source: "暂无数据" };
return { cpu: undefined, memory: undefined, disk: undefined, source: metrics.status === "error" || usage.status === "error" ? "指标不可用" : "等待指标", refreshedAt: undefined };
}, [usage, metrics]);
return (
<div className="console-page">
<div className="console-page operations-overview-page">
<header className="page-header">
<div>
<p className="page-kicker"></p>
<p className="page-kicker"></p>
<h1 className="page-title"></h1>
</div>
<span className={cx("page-status", core.status === "ready" && "page-status-ready")}>
{core.status === "ready" ? "数据已加载" : core.status === "loading" ? "加载中" : "部分模块加载失败"}
</span>
<div className="page-header-actions">
<span className={cx("page-status", moduleFailureCount === 0 && core.status === "ready" && "page-status-ready")}>
{moduleFailureCount > 0 ? `${moduleFailureCount} 个模块不可用` : core.status === "ready" ? "运营数据已同步" : "正在同步"}
</span>
<button type="button" className="icon-command" onClick={refreshAll}>
<RotateCw size={14} />
</button>
</div>
</header>
{core.status === "loading" && <LoadingState label="正在加载服务器与任务概况…" />}
{core.status === "error" && <ErrorState title="平台概况加载失败" reason={core.reason} diagnosticId="overview-core" onRetry={() => void refreshCore()} />}
{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="平台尚未创建任何服务器。前往服务器管理创建第一个实例,或检查运行节点是否在线。"
description={canManageServers ? "平台尚未创建服务器。先确认运行节点在线,再创建第一个实例。" : "当前账号可查看概览,但没有创建服务器的权限。"}
actionLabel="前往服务器管理"
onAction={() => onNavigate("servers")}
/>
)}
{core.status === "ready" && core.data.instances.length > 0 && (
<section className="console-grid" aria-label="platform health">
{core.status === "ready" && (
<section className="console-grid operations-summary-grid" aria-label="平台运营摘要">
<article className="overview-card metric-tone-success">
<span className="metric-label">线</span>
<strong className="metric-value">{onlineCount}</strong>
@@ -225,53 +210,128 @@ export function HomePage({ onNavigate }: PageComponentProps) {
<strong className="metric-value">{offlineCount}</strong>
<p>{core.data.instances.filter((item) => item.state === "failed").length} </p>
</article>
<article className="overview-card metric-tone-neutral">
<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">{core.data.endpoints.filter((item) => item.status === "online").length}</strong>
<p> {core.data.endpoints.length} </p>
<strong className="metric-value">{endpointSummary?.online ?? 0} 线</strong>
<p>{endpointSummary ? `${endpointSummary.degraded} 降级,${endpointSummary.offline} 离线` : "节点状态不可用"}</p>
</article>
<article className={cx("overview-card", errorProviders > 0 ? "metric-tone-warning" : "metric-tone-success")}>
<span className="metric-label">LLM </span>
<strong className="metric-value">
{providers.status === "ready" ? `${activeProviders} 可用` : providers.status === "loading" ? "…" : "未知"}
</strong>
<p>{errorProviders > 0 ? `${errorProviders} 个提供商异常` : "提供商状态正常"}</p>
<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>
<p>{providers.status === "error" ? "提供商状态接口失败" : errorProviders > 0 ? `${errorProviders} 个连接异常` : "连接状态正常"}</p>
</article>
</section>
)}
<section className="operations-command-grid" aria-label="平台运营模块">
<article className="console-panel operations-module" aria-label="任务脉冲">
<div className="panel-header">
<h2><Workflow size={16} /> </h2>
<span className="page-status">{moduleFreshnessLabel(core.status === "ready" ? core.refreshedAt : core.refreshedAt)}</span>
</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" && jobs && (
<>
<dl className="operations-pulse-strip">
<div><dt></dt><dd>{jobs.active.length}</dd></div>
<div><dt></dt><dd>{jobs.retrying.length}</dd></div>
<div><dt></dt><dd>{jobs.failed.length}</dd></div>
</dl>
{jobs.active.length === 0 && jobs.failed.length === 0 ? (
<p className="operations-module-empty"></p>
) : (
<div className="operations-job-list">
{[...jobs.active.slice(0, 3), ...jobs.failed.slice(0, 2)].map((job) => (
<button
key={job.id}
type="button"
className={cx("operations-job-row", job.state === "failed" && "operations-job-row-failed")}
onClick={() => onNavigate(job.serverInstanceId ? "serverDetail" : "maintenance", job.serverInstanceId ? { serverId: job.serverInstanceId } : undefined)}
>
<span><strong>{job.capability}</strong><small>{job.serverInstanceId ? `服务器 ${job.serverInstanceId}` : "平台任务"}</small></span>
<span>{job.state === "retrying" ? `重试 ${job.attempt}/${job.retryPolicy.maxAttempts}` : jobStateLabel(job.state)}</span>
<span>{Math.round(job.progress.percent)}%</span>
</button>
))}
</div>
)}
</>
)}
</article>
<article className="console-panel operations-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="operations-pulse-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="operations-module-empty">Platform </p>
) : (
<div className="operations-endpoint-list">
{core.data.endpoints.slice(0, 4).map((endpoint) => (
<div key={endpoint.id} className="operations-endpoint-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>
<ProductionGovernancePanel compact title="生产容量与告警" />
<section className="overview-two-col">
<article className="console-panel" aria-label="resource usage">
<div className="panel-header">
<h2>
<Activity size={16} style={{ verticalAlign: "-2px" }} />
</h2>
<h2><Activity size={16} /> </h2>
<span className="page-status">{metricAverages.source}</span>
</div>
<div className="server-card-meters">
<UsageMeter label="CPU" percent={metricAverages.cpu} />
<UsageMeter label="内存" percent={metricAverages.memory} />
<UsageMeter label="磁盘" percent={metricAverages.disk} />
</div>
{usage.status === "loading" && metrics.status === "loading" ? (
<LoadingState label="正在读取资源指标…" compact />
) : usage.status === "error" && metrics.status === "error" ? (
<ErrorState title="资源指标不可用" reason={`${usage.reason}${metrics.reason}`} diagnosticId="overview-resource-usage" onRetry={() => { void refreshUsage(); void refreshMetrics(); }} compact />
) : (
<>
<div className="server-card-meters">
<UsageMeter label="CPU" percent={metricAverages.cpu} />
<UsageMeter label="内存" percent={metricAverages.memory} />
<UsageMeter label="磁盘" percent={metricAverages.disk} />
</div>
<span className="operations-module-freshness">{moduleFreshnessLabel(metricAverages.refreshedAt)}</span>
{(usage.status === "error" || metrics.status === "error") && (
<button type="button" className="operations-inline-warning" onClick={() => { void refreshUsage(); void refreshMetrics(); }}>
<AlertTriangle size={14} />
</button>
)}
</>
)}
</article>
<article className="console-panel" aria-label="game type distribution">
<div className="panel-header">
<h2>
<Candy size={16} style={{ verticalAlign: "-2px" }} />
</h2>
</div>
<div className="panel-header"><h2><Candy size={16} /> </h2></div>
{core.status === "loading" ? (
<LoadingState label="统计中…" compact />
) : core.status === "error" ? (
<ErrorState title="分布数据不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />
) : distribution.length === 0 ? (
<EmptyState title="暂无分布数据" description="创建服务器后这里会显示各游戏类型的实例数量。" />
) : (
<div className="action-list">
{distribution.map((entry) => (
<span key={entry.serverType}>
<strong>{entry.label}</strong>{entry.count}
</span>
))}
{distribution.map((entry) => <span key={entry.serverType}><strong>{entry.label}</strong>{entry.count} </span>)}
</div>
)}
</article>
@@ -279,17 +339,16 @@ export function HomePage({ onNavigate }: PageComponentProps) {
<section className="console-panel" aria-label="recent signals">
<div className="panel-header">
<h2>
<MoonStar size={16} style={{ verticalAlign: "-2px" }} />
</h2>
<button type="button" className="icon-command" onClick={() => void refreshSignals()}>
<Sparkles size={14} />
</button>
<h2><MoonStar size={16} /> </h2>
<div className="panel-header-actions">
<span className="page-status">{moduleFreshnessLabel(signals.status === "ready" ? signals.refreshedAt : signals.refreshedAt)}</span>
<button type="button" className="icon-command" onClick={() => void refreshSignals()}><Sparkles size={14} /> </button>
</div>
</div>
{signals.status === "loading" && core.status === "loading" ? (
<LoadingState label="正在收集信号…" compact />
) : overviewSignals.length === 0 ? (
{signals.status === "loading" && core.status === "loading" && <LoadingState label="正在收集信号…" compact />}
{signals.status === "error" && <ErrorState title="审计信号不可用" reason={signals.reason} diagnosticId={signals.diagnosticId} onRetry={() => void refreshSignals()} compact />}
{providers.status === "error" && <ErrorState title="AI 提供商信号不可用" reason={providers.reason} diagnosticId={providers.diagnosticId} onRetry={() => void refreshProviders()} compact />}
{overviewSignals.length === 0 && signals.status === "ready" && core.status === "ready" && providers.status === "ready" ? (
<EmptyState title="暂无异常信号" description="最近没有故障、失败任务或需要关注的审计事件。" />
) : (
<div className="signal-list">
@@ -298,18 +357,10 @@ export function HomePage({ onNavigate }: PageComponentProps) {
key={signal.id}
type="button"
className={cx("signal-item", `signal-tone-${signal.tone}`)}
onClick={() =>
onNavigate(
signal.targetPage === "servers" && signal.targetId ? "serverDetail" : signal.targetPage,
signal.targetPage === "servers" && signal.targetId ? { serverId: signal.targetId } : undefined
)
}
onClick={() => onNavigate(signal.targetPage === "servers" && signal.targetId ? "serverDetail" : signal.targetPage, signal.targetPage === "servers" && signal.targetId ? { serverId: signal.targetId } : undefined)}
>
{signal.tone === "error" ? <AlertTriangle size={18} /> : signal.tone === "warning" ? <AlertTriangle size={18} /> : <Info size={18} />}
<span>
<strong>{signal.summary}</strong>
<p>{signal.detail}</p>
</span>
{signal.tone === "error" || signal.tone === "warning" ? <AlertTriangle size={18} /> : signal.kind === "aiProvider" ? <Bot size={18} /> : <Info size={18} />}
<span><strong>{signal.summary}</strong><p>{signal.detail}</p></span>
<span className="provider-id">{signal.at ? new Date(signal.at).toLocaleString() : ""}</span>
</button>
))}
@@ -319,3 +370,48 @@ export function HomePage({ onNavigate }: PageComponentProps) {
</div>
);
}
function buildOverviewSignals(
core: OperationsModuleState<OverviewData>,
providers: OperationsModuleState<AiProviderResponse[]>,
signals: OperationsModuleState<AuditEventResponse[]>
): PlatformOverviewSignal[] {
const collected: PlatformOverviewSignal[] = [];
if (core.status === "ready") {
for (const instance of core.data.instances.filter((item) => item.state === "failed")) {
collected.push({ id: `fault-${instance.id}`, kind: "fault", summary: `服务器 ${instance.name} 处于异常状态`, detail: `实例 ${instance.id} 需要排查`, targetPage: "servers", targetId: instance.id, tone: "error", at: instance.updatedAt });
}
for (const job of jobBuckets(core.data.jobs).failed.slice(0, 5)) {
collected.push({ id: `job-${job.id}`, kind: "job", summary: `任务 ${job.capability} 失败`, detail: job.serverInstanceId ? `服务器 ${job.serverInstanceId} 的任务执行失败` : "平台任务执行失败", targetPage: job.serverInstanceId ? "servers" : "maintenance", targetId: job.serverInstanceId, tone: "error", at: job.updatedAt });
}
}
if (providers.status === "ready") {
for (const provider of providers.data.filter((item) => item.status === "error")) {
collected.push({ id: `ai-${provider.id}`, kind: "aiProvider", summary: `AI 提供商 ${provider.name} 连接异常`, detail: "LLM 辅助暂不可用,请在提供商管理中检查已保存配置。", targetPage: "aiProviders", targetId: provider.id, tone: "warning", at: "" });
}
}
if (signals.status === "ready") {
for (const event of signals.data.slice(0, 5)) {
collected.push({ id: `audit-${event.id}`, kind: "log", summary: event.summary || `${event.action} ${event.resourceKind}`, detail: `${event.resourceKind}/${event.resourceId}${event.result}`, targetPage: event.resourceKind === "server-instance" ? "servers" : "maintenance", targetId: event.resourceId, tone: event.result === "failure" ? "warning" : "info", at: event.createdAt });
}
}
return collected.sort((left, right) => Date.parse(right.at || "") - Date.parse(left.at || "")).slice(0, 8);
}
function refreshedNow(): string {
return new Date().toISOString();
}
function errorMessage(error: unknown, fallback: string): string {
return error instanceof Error ? error.message : fallback;
}
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];
}