import type { JobResponse, RunEndpointResponse, ServerInstanceResponse, ServerMetricsResponse } from "../api/types"; import { safeDiagnosticText } from "../utils/safeDiagnosticText"; import type { OperationRecord } from "./workspace"; export type OperationsModuleState = | { status: "loading"; refreshedAt?: string } | { status: "error"; reason: string; diagnosticId: string; refreshedAt?: string } | { status: "ready"; data: T; refreshedAt: string }; export interface JobBuckets { active: JobResponse[]; retrying: JobResponse[]; failed: JobResponse[]; succeeded: JobResponse[]; cancelled: JobResponse[]; } export interface ServerOperationsSummary { instance: ServerInstanceResponse; metrics?: ServerMetricsResponse; activeJobs: number; failedJobs: number; latestJob?: JobResponse; } export interface EndpointOperationsSummary { total: number; online: number; degraded: number; offline: number; disabled: number; activeJobs: number; queuedJobs: number; } export interface OperationTrayItem { id: string; intent: string; targetLabel: string; status: OperationRecord["status"]; message?: string; errorReason?: string; diagnosticId?: string; updatedAt: string; } const activeJobStates = new Set(["queued", "accepted", "running", "retrying"]); export function isActiveJob(job: JobResponse): boolean { return activeJobStates.has(job.state); } export function jobBuckets(jobs: JobResponse[]): JobBuckets { const sorted = [...jobs].sort((left, right) => timestamp(right.updatedAt) - timestamp(left.updatedAt)); return { active: sorted.filter(isActiveJob), retrying: sorted.filter((job) => job.state === "retrying"), failed: sorted.filter((job) => job.state === "failed"), succeeded: sorted.filter((job) => job.state === "succeeded"), cancelled: sorted.filter((job) => job.state === "cancelled") }; } export function summarizeServerOperations( instances: ServerInstanceResponse[], metrics: Map, jobs: JobResponse[] ): ServerOperationsSummary[] { const jobsByServer = new Map(); for (const job of jobs) { if (!job.serverInstanceId) { continue; } const serverJobs = jobsByServer.get(job.serverInstanceId) ?? []; serverJobs.push(job); jobsByServer.set(job.serverInstanceId, serverJobs); } return instances .map((instance) => { const serverJobs = [...(jobsByServer.get(instance.id) ?? [])].sort( (left, right) => timestamp(right.updatedAt) - timestamp(left.updatedAt) ); return { instance, metrics: metrics.get(instance.id), activeJobs: serverJobs.filter(isActiveJob).length, failedJobs: serverJobs.filter((job) => job.state === "failed").length, latestJob: serverJobs[0] }; }) .sort(compareServerOperations); } export function summarizeEndpointOperations(endpoints: RunEndpointResponse[]): EndpointOperationsSummary { return endpoints.reduce( (summary, endpoint) => ({ total: summary.total + 1, online: summary.online + Number(endpoint.status === "online"), degraded: summary.degraded + Number(endpoint.status === "degraded"), offline: summary.offline + Number(endpoint.status === "offline"), disabled: summary.disabled + Number(endpoint.status === "disabled"), activeJobs: summary.activeJobs + endpoint.capacity.runningJobs, queuedJobs: summary.queuedJobs + endpoint.capacity.queuedJobs }), { total: 0, online: 0, degraded: 0, offline: 0, disabled: 0, activeJobs: 0, queuedJobs: 0 } ); } export function projectOperationForTray(operation: OperationRecord): OperationTrayItem { return { id: operation.id, intent: operation.intent, targetLabel: safeOperationTarget(operation.targetKind, operation.targetId), status: operation.status, message: safeDiagnosticText(operation.message), errorReason: safeDiagnosticText(operation.errorReason), diagnosticId: safeDiagnosticId(operation.diagnosticId), updatedAt: operation.updatedAt }; } export function moduleFreshnessLabel(refreshedAt?: string): string { if (!refreshedAt) { return "尚未刷新"; } const time = new Date(refreshedAt); return Number.isNaN(time.getTime()) ? "刷新时间未知" : `刷新于 ${time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`; } function compareServerOperations(left: ServerOperationsSummary, right: ServerOperationsSummary): number { const attentionDifference = serverAttentionRank(right) - serverAttentionRank(left); if (attentionDifference !== 0) { return attentionDifference; } const updatedDifference = timestamp(right.instance.updatedAt) - timestamp(left.instance.updatedAt); return updatedDifference !== 0 ? updatedDifference : left.instance.name.localeCompare(right.instance.name, "zh-CN"); } function serverAttentionRank(summary: ServerOperationsSummary): number { if (summary.instance.state === "failed") { return 50; } if (summary.failedJobs > 0) { return 40; } if (summary.instance.state === "installing") { return 30; } if (summary.activeJobs > 0) { return 20; } if (summary.instance.state === "running") { return 10; } return 0; } function safeOperationTarget(kind: OperationRecord["targetKind"], targetId: string): string { const kindLabel: Record = { server: "服务器", plugin: "插件", config: "配置", llm: "AI 提供商", platform: "平台" }; const safeId = /^[a-zA-Z0-9._:-]{1,96}$/.test(targetId) ? targetId : "受保护目标"; return `${kindLabel[kind]} / ${safeId}`; } function safeDiagnosticId(value: string | undefined): string | undefined { return value && /^[a-zA-Z0-9._:-]{1,96}$/.test(value) ? value : value ? "受保护诊断" : undefined; } function timestamp(value: string): number { const parsed = Date.parse(value); return Number.isNaN(parsed) ? 0 : parsed; }