feat(workbench): integrate runtime data and map tooling
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
import { z } from "zod";
|
||||
import type { FeatureCollection, Geometry, Position } from "geojson";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { env } from "@/shared/config/env";
|
||||
import type {
|
||||
ScheduledConditionRecord,
|
||||
ScheduledWorkOrderItem,
|
||||
WorkbenchAlert,
|
||||
WorkbenchScenario
|
||||
} from "../types";
|
||||
import type { AnalysisArtifact, ArtifactLifecycle, ArtifactType } from "../workspace/workspace-model";
|
||||
import { createWorkbenchHeaders, findOwnedWorkbenchProject, getServerError } from "./project-client";
|
||||
|
||||
// Keep legacy analysis values parseable while old servers are rolling forward;
|
||||
// they are filtered before the operational workbench model is created.
|
||||
const runtimeConditionSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
task_id: z.enum(["backend-analysis", "backend-schedule"]),
|
||||
session_id: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
summary: z.string().trim().min(1),
|
||||
status: z.enum(["pending", "running", "completed", "warning", "error"]),
|
||||
risk_level: z.enum(["normal", "attention", "critical"]),
|
||||
scheduled_at: z.string().datetime({ offset: true }),
|
||||
updated_at: z.string().datetime({ offset: true }),
|
||||
detail: z.string().trim().min(1),
|
||||
recommendation: z.string().trim().min(1).nullable(),
|
||||
evidence: z.array(z.string().trim().min(1)),
|
||||
model_name: z.enum(["TJWater analysis runtime", "TJWater scheduler"])
|
||||
});
|
||||
|
||||
type RuntimeConditionPayload = z.infer<typeof runtimeConditionSchema>;
|
||||
type OperationalRuntimeConditionPayload = RuntimeConditionPayload & {
|
||||
task_id: "backend-schedule";
|
||||
};
|
||||
|
||||
const runtimeAlertSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
condition_id: z.string().trim().min(1).nullable(),
|
||||
source: z.object({
|
||||
task_type: z.enum(["scheduled_task", "analysis_task", "other_task"]),
|
||||
task_id: z.string().trim().min(1),
|
||||
run_id: z.string().trim().min(1)
|
||||
}),
|
||||
title: z.string().trim().min(1),
|
||||
description: z.string().trim().min(1),
|
||||
occurred_at: z.string().datetime({ offset: true }),
|
||||
severity: z.enum(["warning", "critical"])
|
||||
});
|
||||
|
||||
const runtimeScenarioSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
run_id: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: z.string().trim().min(1),
|
||||
status: z.enum(["draft", "active", "review"]),
|
||||
run_type: z.string().trim().min(1),
|
||||
run_type_label: z.string().trim().min(1),
|
||||
run_status: z.string().trim().min(1),
|
||||
created_at: z.string().datetime({ offset: true })
|
||||
});
|
||||
|
||||
const runtimeAnalysisResultSchema = z.object({
|
||||
result_type: z.string().trim().min(1),
|
||||
created_at: z.string().datetime({ offset: true }),
|
||||
facts: z.array(z.object({
|
||||
label: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1)
|
||||
}))
|
||||
});
|
||||
|
||||
const geoJsonFeatureCollectionSchema = z.custom<FeatureCollection<Geometry>>(
|
||||
isGeoJsonFeatureCollection
|
||||
);
|
||||
|
||||
const runtimeLatestAnalysisSchema = z.object({
|
||||
run_id: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
run_type: z.string().trim().min(1),
|
||||
run_type_label: z.string().trim().min(1),
|
||||
status: z.string().trim().min(1),
|
||||
status_label: z.string().trim().min(1),
|
||||
created_by: z.string().trim().min(1),
|
||||
created_at: z.string().datetime({ offset: true }),
|
||||
started_at: z.string().datetime({ offset: true }),
|
||||
results: z.array(runtimeAnalysisResultSchema),
|
||||
map_payload: z.unknown().nullable()
|
||||
});
|
||||
|
||||
const runtimeWorkOrderStageSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
sequence: z.number().int().positive(),
|
||||
title: z.string().trim().min(1),
|
||||
status: z.enum(["done", "current", "pending", "blocked", "skipped"]),
|
||||
owner: z.string().trim().min(1),
|
||||
time_label: z.string().trim().min(1),
|
||||
description: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
const runtimeWorkOrderSubtaskSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
summary: z.string().trim().min(1),
|
||||
status: z.enum(["pending", "running", "blocked", "completed", "cancelled"]),
|
||||
scheduled_at: z.string().datetime({ offset: true }),
|
||||
location: z.string().trim().min(1),
|
||||
assignee: z.string().trim().min(1),
|
||||
duration_minutes: z.number().int().positive(),
|
||||
reply_window_minutes: z.number().int().positive(),
|
||||
stages: z.array(runtimeWorkOrderStageSchema).min(1),
|
||||
reply_requirements: z.array(z.string().trim().min(1))
|
||||
}).superRefine((subtask, context) => {
|
||||
if (subtask.stages.some((stage, index) => stage.sequence !== index + 1)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "子任务阶段序号必须从 1 连续递增",
|
||||
path: ["stages"]
|
||||
});
|
||||
}
|
||||
if (new Set(subtask.stages.map((stage) => stage.id)).size !== subtask.stages.length) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "子任务阶段编号不能重复",
|
||||
path: ["stages"]
|
||||
});
|
||||
}
|
||||
if (subtask.stages.filter((stage) => stage.status === "current").length > 1) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "单个子任务最多只能有一个当前阶段",
|
||||
path: ["stages"]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const runtimeWorkOrderSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
code: z.string().trim().min(1),
|
||||
scheduled_at: z.string().datetime({ offset: true }),
|
||||
title: z.string().trim().min(1),
|
||||
summary: z.string().trim().min(1),
|
||||
status: z.enum(["pending", "running", "awaiting_reply", "completed"]),
|
||||
risk_level: z.enum(["normal", "attention", "critical"]),
|
||||
updated_at: z.string().datetime({ offset: true }),
|
||||
detail: z.string().trim().min(1),
|
||||
recommendation: z.string().trim().min(1).nullable(),
|
||||
duration_minutes: z.number().int().positive(),
|
||||
source: z.string().trim().min(1),
|
||||
dispatcher: z.string().trim().min(1),
|
||||
priority: z.enum(["normal", "urgent"]),
|
||||
subtasks: z.array(runtimeWorkOrderSubtaskSchema).min(1)
|
||||
}).superRefine((workOrder, context) => {
|
||||
if (new Set(workOrder.subtasks.map((subtask) => subtask.id)).size !== workOrder.subtasks.length) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "工单子任务编号不能重复",
|
||||
path: ["subtasks"]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const runtimeSnapshotSchema = z.object({
|
||||
availability: z.enum(["ready", "not_configured", "degraded"]),
|
||||
source: z.enum(["none", "analysis_runs", "scheduled_tasks"]),
|
||||
scenario_availability: z.enum(["ready", "not_configured", "degraded"]),
|
||||
artifact_availability: z.enum(["ready", "not_configured", "degraded"]),
|
||||
schedule_availability: z.enum(["ready", "not_configured", "degraded"]),
|
||||
work_order_availability: z.enum(["ready", "not_configured", "degraded"]),
|
||||
alert_availability: z.enum(["ready", "not_configured", "degraded"]),
|
||||
generated_at: z.string().datetime({ offset: true }),
|
||||
conditions: z.array(runtimeConditionSchema),
|
||||
alerts: z.array(runtimeAlertSchema),
|
||||
scenarios: z.array(runtimeScenarioSchema),
|
||||
latest_analysis: runtimeLatestAnalysisSchema.nullable(),
|
||||
work_orders: z.array(runtimeWorkOrderSchema)
|
||||
});
|
||||
|
||||
type ProjectionAvailability = "ready" | "not_configured" | "degraded";
|
||||
|
||||
export type WorkbenchRuntimeSnapshot = {
|
||||
availability: ProjectionAvailability;
|
||||
source: "none" | "scheduled_tasks";
|
||||
scenarioAvailability: ProjectionAvailability;
|
||||
artifactAvailability: ProjectionAvailability;
|
||||
scheduleAvailability: ProjectionAvailability;
|
||||
workOrderAvailability: ProjectionAvailability;
|
||||
alertAvailability: ProjectionAvailability;
|
||||
generatedAt: string;
|
||||
conditions: ScheduledConditionRecord[];
|
||||
alerts: WorkbenchAlert[];
|
||||
scenarios: WorkbenchScenario[];
|
||||
latestAnalysisArtifact: AnalysisArtifact | null;
|
||||
simulationOverlay: FeatureCollection<Geometry> | null;
|
||||
workOrders: ScheduledWorkOrderItem[];
|
||||
};
|
||||
|
||||
export type WorkbenchAnalysisProjection = {
|
||||
runId: string;
|
||||
artifact: AnalysisArtifact;
|
||||
simulationOverlay: FeatureCollection<Geometry> | null;
|
||||
};
|
||||
|
||||
const SERVER_API_BASE_URL = env.TJWATER_SERVER_API_BASE_URL.replace(/\/$/, "");
|
||||
|
||||
export async function fetchWorkbenchRuntimeSnapshot(
|
||||
getAccessToken?: AccessTokenProvider,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkbenchRuntimeSnapshot> {
|
||||
const { project, accessToken } = await findOwnedWorkbenchProject(getAccessToken, signal);
|
||||
const response = await fetch(`${SERVER_API_BASE_URL}/api/v1/workbench/runtime`, {
|
||||
headers: createWorkbenchHeaders(accessToken, project.project_id),
|
||||
signal
|
||||
});
|
||||
const payload: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(getServerError(payload, response.status, "工作台运行态请求"));
|
||||
}
|
||||
const snapshot = runtimeSnapshotSchema.parse(payload);
|
||||
const latestAnalysis = snapshot.latest_analysis
|
||||
? projectWorkbenchAnalysis(snapshot.latest_analysis)
|
||||
: null;
|
||||
const operationalConditions = snapshot.conditions.filter(isOperationalRuntimeCondition);
|
||||
|
||||
return {
|
||||
availability: snapshot.availability,
|
||||
source: resolveOperationalRuntimeSource(snapshot, operationalConditions),
|
||||
scenarioAvailability: snapshot.scenario_availability,
|
||||
artifactAvailability: snapshot.artifact_availability,
|
||||
scheduleAvailability: snapshot.schedule_availability,
|
||||
workOrderAvailability: snapshot.work_order_availability,
|
||||
alertAvailability: snapshot.alert_availability,
|
||||
generatedAt: snapshot.generated_at,
|
||||
conditions: operationalConditions.map((condition) => ({
|
||||
id: condition.id,
|
||||
kind: "condition",
|
||||
taskId: condition.task_id,
|
||||
dataSource: getConditionDataSource(condition.task_id),
|
||||
sessionId: condition.session_id,
|
||||
title: condition.title,
|
||||
summary: condition.summary,
|
||||
status: condition.status,
|
||||
riskLevel: condition.risk_level,
|
||||
scheduledAt: condition.scheduled_at,
|
||||
updatedAt: Date.parse(condition.updated_at),
|
||||
detail: condition.detail,
|
||||
recommendation: condition.recommendation ?? undefined,
|
||||
evidence: condition.evidence,
|
||||
modelName: condition.model_name
|
||||
})),
|
||||
alerts: snapshot.alerts.map((alert) => ({
|
||||
...(alert.condition_id ? { conditionId: alert.condition_id } : {}),
|
||||
id: alert.id,
|
||||
title: alert.title,
|
||||
description: alert.description,
|
||||
source: {
|
||||
taskType: alert.source.task_type,
|
||||
taskId: alert.source.task_id,
|
||||
runId: alert.source.run_id
|
||||
},
|
||||
severity: alert.severity,
|
||||
occurredAt: alert.occurred_at,
|
||||
time: formatRuntimeAlertTime(alert.occurred_at)
|
||||
})),
|
||||
scenarios: snapshot.scenarios.map((scenario) => ({
|
||||
id: scenario.id,
|
||||
name: scenario.name,
|
||||
description: scenario.description,
|
||||
status: scenario.status,
|
||||
runId: scenario.run_id,
|
||||
runType: scenario.run_type,
|
||||
runStatus: scenario.run_status,
|
||||
createdAt: scenario.created_at
|
||||
})),
|
||||
latestAnalysisArtifact: latestAnalysis?.artifact ?? null,
|
||||
simulationOverlay: latestAnalysis?.simulationOverlay ?? null,
|
||||
workOrders: snapshot.work_orders.map((workOrder) => ({
|
||||
id: workOrder.id,
|
||||
kind: "work_order",
|
||||
code: workOrder.code,
|
||||
scheduledAt: workOrder.scheduled_at,
|
||||
title: workOrder.title,
|
||||
summary: workOrder.summary,
|
||||
status: workOrder.status,
|
||||
riskLevel: workOrder.risk_level,
|
||||
updatedAt: Date.parse(workOrder.updated_at),
|
||||
detail: workOrder.detail,
|
||||
recommendation: workOrder.recommendation ?? undefined,
|
||||
durationMinutes: workOrder.duration_minutes,
|
||||
source: workOrder.source,
|
||||
dispatcher: workOrder.dispatcher,
|
||||
priority: workOrder.priority,
|
||||
subtasks: workOrder.subtasks.map((subtask) => ({
|
||||
id: subtask.id,
|
||||
title: subtask.title,
|
||||
summary: subtask.summary,
|
||||
status: subtask.status,
|
||||
scheduledAt: subtask.scheduled_at,
|
||||
location: subtask.location,
|
||||
assignee: subtask.assignee,
|
||||
durationMinutes: subtask.duration_minutes,
|
||||
replyWindowMinutes: subtask.reply_window_minutes,
|
||||
stages: subtask.stages.map((stage) => ({
|
||||
id: stage.id,
|
||||
sequence: stage.sequence,
|
||||
title: stage.title,
|
||||
status: stage.status,
|
||||
owner: stage.owner,
|
||||
timeLabel: stage.time_label,
|
||||
description: stage.description
|
||||
})),
|
||||
replyRequirements: subtask.reply_requirements
|
||||
}))
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchWorkbenchAnalysisRun(
|
||||
runId: string,
|
||||
getAccessToken?: AccessTokenProvider,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkbenchAnalysisProjection> {
|
||||
const normalizedRunId = runId.trim();
|
||||
if (!normalizedRunId) throw new Error("分析运行 ID 不能为空");
|
||||
const { project, accessToken } = await findOwnedWorkbenchProject(getAccessToken, signal);
|
||||
const response = await fetch(
|
||||
`${SERVER_API_BASE_URL}/api/v1/workbench/analysis-runs/${encodeURIComponent(normalizedRunId)}`,
|
||||
{
|
||||
headers: createWorkbenchHeaders(accessToken, project.project_id),
|
||||
signal
|
||||
}
|
||||
);
|
||||
const payload: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(getServerError(payload, response.status, "分析运行成果请求"));
|
||||
}
|
||||
const analysis = runtimeLatestAnalysisSchema.parse(payload);
|
||||
if (analysis.run_id !== normalizedRunId) {
|
||||
throw new Error("分析运行成果与请求 ID 不一致");
|
||||
}
|
||||
return projectWorkbenchAnalysis(analysis);
|
||||
}
|
||||
|
||||
function projectWorkbenchAnalysis(
|
||||
analysis: z.infer<typeof runtimeLatestAnalysisSchema>
|
||||
): WorkbenchAnalysisProjection {
|
||||
const parsedSimulationOverlay = analysis.map_payload
|
||||
? geoJsonFeatureCollectionSchema.safeParse(analysis.map_payload)
|
||||
: null;
|
||||
const simulationOverlay = parsedSimulationOverlay?.success
|
||||
? parsedSimulationOverlay.data
|
||||
: null;
|
||||
return {
|
||||
runId: analysis.run_id,
|
||||
artifact: createPersistedAnalysisArtifact(analysis, simulationOverlay),
|
||||
simulationOverlay
|
||||
};
|
||||
}
|
||||
|
||||
function createPersistedAnalysisArtifact(
|
||||
analysis: z.infer<typeof runtimeLatestAnalysisSchema>,
|
||||
simulationOverlay: FeatureCollection<Geometry> | null
|
||||
): AnalysisArtifact {
|
||||
const resultTypes = [...new Set(analysis.results.map((result) => result.result_type))];
|
||||
const completed = ["completed", "succeeded"].includes(analysis.status);
|
||||
const lifecycle: ArtifactLifecycle = completed ? "reviewed" : "draft";
|
||||
const artifactType = getArtifactType(analysis.run_type);
|
||||
const factCount = analysis.results.reduce((total, result) => total + result.facts.length, 0);
|
||||
const resultSummary = analysis.results.length > 0
|
||||
? `后端记录状态为${analysis.status_label},已持久化 ${analysis.results.length} 条分析结果。`
|
||||
: `后端记录状态为${analysis.status_label},当前未查询到独立结果记录。`;
|
||||
|
||||
return {
|
||||
id: `persisted-analysis-${analysis.run_id}`,
|
||||
origin: "backend",
|
||||
type: artifactType,
|
||||
lifecycle,
|
||||
revision: 1,
|
||||
title: analysis.name,
|
||||
subtitle: `${analysis.run_type_label} · 项目后端分析运行`,
|
||||
summary: resultSummary,
|
||||
scope: `运行 ${analysis.run_id}`,
|
||||
confidence: "后端未提供",
|
||||
generatedAt: formatRuntimeDateTime(analysis.created_at),
|
||||
analyticalTimeRange: {
|
||||
start: analysis.started_at,
|
||||
end: analysis.created_at
|
||||
},
|
||||
preferredView: simulationOverlay ? "map_split" : "focus",
|
||||
mapRelation: simulationOverlay ? "required" : "none",
|
||||
...(simulationOverlay ? { spatialOverlay: simulationOverlay } : {}),
|
||||
blocks: [
|
||||
{
|
||||
id: `persisted-analysis-${analysis.run_id}-metrics`,
|
||||
kind: "metric-grid",
|
||||
title: "运行摘要",
|
||||
span: 12,
|
||||
metrics: [
|
||||
{
|
||||
label: "后端状态",
|
||||
value: analysis.status_label,
|
||||
detail: analysis.status,
|
||||
tone: getStatusTone(analysis.status)
|
||||
},
|
||||
{
|
||||
label: "结果记录",
|
||||
value: String(analysis.results.length),
|
||||
detail: "analysis.results",
|
||||
tone: analysis.results.length > 0 ? "success" : "neutral"
|
||||
},
|
||||
{
|
||||
label: "结果类型",
|
||||
value: String(resultTypes.length),
|
||||
detail: resultTypes.join("、") || "暂无",
|
||||
tone: "info"
|
||||
},
|
||||
{
|
||||
label: "成果事实",
|
||||
value: String(factCount),
|
||||
detail: factCount > 0 ? "后端已识别字段" : "暂无已识别字段",
|
||||
tone: factCount > 0 ? "success" : "neutral"
|
||||
},
|
||||
{
|
||||
label: "创建人",
|
||||
value: analysis.created_by,
|
||||
detail: analysis.run_type_label,
|
||||
tone: "neutral"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: `persisted-analysis-${analysis.run_id}-metadata`,
|
||||
kind: "narrative",
|
||||
title: "后端运行记录",
|
||||
span: 12,
|
||||
paragraphs: [
|
||||
`运行 ID:${analysis.run_id}`,
|
||||
`分析类型:${analysis.run_type_label}(${analysis.run_type})`,
|
||||
`创建人:${analysis.created_by};状态:${analysis.status_label}。`
|
||||
]
|
||||
},
|
||||
...(analysis.results.length > 0
|
||||
? [{
|
||||
id: `persisted-analysis-${analysis.run_id}-results`,
|
||||
kind: "data-table" as const,
|
||||
title: "持久化结果索引",
|
||||
description: "来自项目 BizDB analysis.results 的结果记录",
|
||||
span: 12 as const,
|
||||
columns: [
|
||||
{ key: "type", label: "结果类型" },
|
||||
{ key: "createdAt", label: "写入时间" },
|
||||
{ key: "facts", label: "后端成果事实" }
|
||||
],
|
||||
rows: analysis.results.map((result) => ({
|
||||
type: result.result_type,
|
||||
createdAt: formatRuntimeDateTime(result.created_at),
|
||||
facts: result.facts.length > 0
|
||||
? result.facts.map((fact) => `${fact.label}:${fact.value}`).join(";")
|
||||
: "未提供已识别字段"
|
||||
}))
|
||||
}]
|
||||
: [])
|
||||
],
|
||||
sources: [
|
||||
"项目 BizDB · analysis.runs",
|
||||
...(analysis.results.length > 0 ? ["项目 BizDB · analysis.results"] : [])
|
||||
],
|
||||
limitations: [
|
||||
factCount > 0
|
||||
? "当前投影展示后端持久化元数据、结果索引和已识别的成果字段;未透传任意结果载荷。"
|
||||
: "当前投影只展示后端持久化元数据与结果索引。",
|
||||
"后端记录没有提供综合置信度。"
|
||||
],
|
||||
actions: []
|
||||
};
|
||||
}
|
||||
|
||||
function getArtifactType(runType: string): ArtifactType {
|
||||
if (["burst_detection", "burst_location", "dma_leak_identification"].includes(runType)) {
|
||||
return "diagnosis";
|
||||
}
|
||||
if (runType === "sensor_placement") return "metric_analysis";
|
||||
return "simulation";
|
||||
}
|
||||
|
||||
function getStatusTone(status: string) {
|
||||
if (["completed", "succeeded"].includes(status)) return "success" as const;
|
||||
if (["failed", "error"].includes(status)) return "danger" as const;
|
||||
if (status === "running") return "info" as const;
|
||||
return "warning" as const;
|
||||
}
|
||||
|
||||
function formatRuntimeAlertTime(value: string) {
|
||||
const date = new Date(value);
|
||||
return `${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function getConditionDataSource(
|
||||
_taskId: OperationalRuntimeConditionPayload["task_id"]
|
||||
) {
|
||||
return "scheduled_tasks" as const;
|
||||
}
|
||||
|
||||
function isOperationalRuntimeCondition(
|
||||
condition: RuntimeConditionPayload
|
||||
): condition is OperationalRuntimeConditionPayload {
|
||||
return condition.task_id === "backend-schedule";
|
||||
}
|
||||
|
||||
function resolveOperationalRuntimeSource(
|
||||
snapshot: z.infer<typeof runtimeSnapshotSchema>,
|
||||
conditions: OperationalRuntimeConditionPayload[]
|
||||
): WorkbenchRuntimeSnapshot["source"] {
|
||||
if (snapshot.source === "scheduled_tasks") return "scheduled_tasks";
|
||||
return conditions.length > 0 ? "scheduled_tasks" : "none";
|
||||
}
|
||||
|
||||
function formatRuntimeDateTime(value: string) {
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function isGeoJsonFeatureCollection(value: unknown): value is FeatureCollection<Geometry> {
|
||||
if (!isRecord(value) || value.type !== "FeatureCollection" || !Array.isArray(value.features)) {
|
||||
return false;
|
||||
}
|
||||
return value.features.every((feature) =>
|
||||
isRecord(feature)
|
||||
&& feature.type === "Feature"
|
||||
&& (feature.properties === null || isRecord(feature.properties))
|
||||
&& (feature.geometry === null || isGeoJsonGeometry(feature.geometry))
|
||||
);
|
||||
}
|
||||
|
||||
function isGeoJsonGeometry(value: unknown): value is Geometry {
|
||||
if (!isRecord(value) || typeof value.type !== "string") return false;
|
||||
if (value.type === "GeometryCollection") {
|
||||
return Array.isArray(value.geometries) && value.geometries.every(isGeoJsonGeometry);
|
||||
}
|
||||
const coordinates = value.coordinates;
|
||||
if (value.type === "Point") return isPosition(coordinates);
|
||||
if (value.type === "MultiPoint" || value.type === "LineString") {
|
||||
return isPositionArray(coordinates);
|
||||
}
|
||||
if (value.type === "MultiLineString" || value.type === "Polygon") {
|
||||
return Array.isArray(coordinates) && coordinates.every(isPositionArray);
|
||||
}
|
||||
if (value.type === "MultiPolygon") {
|
||||
return Array.isArray(coordinates)
|
||||
&& coordinates.every((polygon) =>
|
||||
Array.isArray(polygon) && polygon.every(isPositionArray)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPosition(value: unknown): value is Position {
|
||||
return Array.isArray(value)
|
||||
&& value.length >= 2
|
||||
&& value.every((coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate));
|
||||
}
|
||||
|
||||
function isPositionArray(value: unknown): value is Position[] {
|
||||
return Array.isArray(value) && value.every(isPosition);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user