feat(platform): harden telemetry pipeline and unify Semi UI workspaces

This commit is contained in:
lingniu
2026-07-18 00:26:36 +08:00
parent 65b4e4f055
commit 159c80b0ae
136 changed files with 21616 additions and 1785 deletions

View File

@@ -10,6 +10,7 @@ describe('history domain', () => {
it('formats units without fabricating missing values', () => {
expect(formatHistoryValue(undefined)).toBe('—');
expect(formatHistoryValue(42.5, { unit: 'km/h' } as never)).toBe('42.5 km/h');
expect(formatHistoryValue(1, { valueMappings: [{ value: '1', label: '启动' }] } as never)).toBe('启动1');
});
it('builds only numeric series with at least two evidence points', () => {

View File

@@ -24,6 +24,9 @@ export function parseHistoryKeywords(value: string) {
export function formatHistoryValue(value: unknown, metric?: HistoryMetricDefinition) {
if (value == null || value === '') return '—';
const raw = typeof value === 'number' && Number.isInteger(value) ? String(value) : String(value);
const mapped = metric?.valueMappings?.find((item) => item.value === raw);
if (mapped) return `${mapped.label}${mapped.value}`;
const formatted = typeof value === 'number' ? formatZhNumber(value, 6) : String(value);
return metric?.unit ? `${formatted} ${metric.unit}` : formatted;
}

View File

@@ -1,6 +1,6 @@
import type { VehicleRealtimeRow } from '../../api/types';
import { describe, expect, it } from 'vitest';
import { formatNumber, statusLabel, vehicleStatus } from './monitor';
import { formatNumber, normalizeMonitorBounds, statusLabel, vehicleStatus } from './monitor';
function vehicle(overrides: Partial<VehicleRealtimeRow> = {}): VehicleRealtimeRow {
return {
@@ -37,4 +37,11 @@ describe('monitor domain', () => {
expect(formatNumber(12560)).toBe('12,560');
expect(statusLabel('driving')).toBe('行驶');
});
it('normalizes valid WGS-84 viewport bounds and drops unsafe map ranges', () => {
expect(normalizeMonitorBounds('113,22,114,24')).toBe('113.000000,22.000000,114.000000,24.000000');
expect(normalizeMonitorBounds('105,31,103,29')).toBe('');
expect(normalizeMonitorBounds('-181,0,181,1')).toBe('');
expect(normalizeMonitorBounds('113,22,Infinity,24')).toBe('');
});
});

View File

@@ -34,3 +34,12 @@ export function relativeFreshness(value: string) {
export function formatNumber(value: number, maximumFractionDigits = 0) {
return formatZhNumber(value, maximumFractionDigits);
}
export function normalizeMonitorBounds(value?: string | null) {
if (!value) return '';
const coordinates = value.split(',').map(Number);
if (coordinates.length !== 4 || coordinates.some((coordinate) => !Number.isFinite(coordinate))) return '';
const [west, south, east, north] = coordinates;
if (west < -180 || east > 180 || south < -90 || north > 90 || west >= east || south >= north) return '';
return coordinates.map((coordinate) => coordinate.toFixed(6)).join(',');
}

View File

@@ -3,15 +3,15 @@ import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from './profi
describe('parseVehicleProfileSyncCSV', () => {
it('parses quoted values, BOM, CRLF and normalizes VIN/status', () => {
const rows = parseVehicleProfileSyncCSV(`\uFEFF${vehicleProfileSyncCSVHeader}\r\nvin001,"车型,一",重卡,示范物流,ACTIVE,车厂平台,2026-07-01T08:30:00+08:00,3600\r\n`);
expect(rows).toEqual([expect.objectContaining({ vin: 'VIN001', modelName: '车型,一', operationStatus: 'active', runtimeSeconds: 3600 })]);
const rows = parseVehicleProfileSyncCSV(`\uFEFF${vehicleProfileSyncCSVHeader}\r\nvin001,宇通,"车型,一",重卡,示范物流,ACTIVE,车厂平台,2026-07-01T08:30:00+08:00,3600\r\n`);
expect(rows).toEqual([expect.objectContaining({ vin: 'VIN001', brandName: '宇通', modelName: '车型,一', operationStatus: 'active', runtimeSeconds: 3600 })]);
});
it('rejects duplicate VINs and malformed source rows before upload', () => {
const duplicate = `${vehicleProfileSyncCSVHeader}\nVIN001,,,,unknown,,,\nvin001,,,,unknown,,,`;
const duplicate = `${vehicleProfileSyncCSVHeader}\nVIN001,,,,,unknown,,,\nvin001,,,,,unknown,,,`;
expect(() => parseVehicleProfileSyncCSV(duplicate)).toThrow(/VIN 重复/);
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,,,,active,,,1.5`)).toThrow(/累计运行秒数/);
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,,,,,active,,,1.5`)).toThrow(/累计运行秒数/);
expect(() => parseVehicleProfileSyncCSV('vin,modelName\nVIN001,车型')).toThrow(/表头/);
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,"车型"x,,,active,,,`)).toThrow(/引号结束/);
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,宇通,"车型"x,,,active,,,`)).toThrow(/引号结束/);
});
});

View File

@@ -1,6 +1,6 @@
import type { VehicleProfileSyncItem } from '../../api/types';
const headers = ['vin', 'modelName', 'vehicleType', 'companyName', 'operationStatus', 'accessProvider', 'firstAccessAt', 'runtimeSeconds'] as const;
const headers = ['vin', 'brandName', 'modelName', 'vehicleType', 'companyName', 'operationStatus', 'accessProvider', 'firstAccessAt', 'runtimeSeconds'] as const;
const allowedStatuses = new Set(['', 'unknown', 'active', 'inactive', 'maintenance', 'retired']);
export const vehicleProfileSyncCSVHeader = headers.join(',');
@@ -18,7 +18,7 @@ export function parseVehicleProfileSyncCSV(text: string): VehicleProfileSyncItem
return dataRows.map((row, index) => {
const line = index + 2;
if (row.length !== headers.length) throw new Error(`CSV 第 ${line} 行列数不正确`);
const [rawVIN, modelName, vehicleType, companyName, rawStatus, accessProvider, firstAccessAt, rawRuntime] = row.map((value) => value.trim());
const [rawVIN, brandName, modelName, vehicleType, companyName, rawStatus, accessProvider, firstAccessAt, rawRuntime] = row.map((value) => value.trim());
const vin = rawVIN.toUpperCase();
if (!vin || vin.length > 32) throw new Error(`CSV 第 ${line} 行 VIN 无效`);
if (seen.has(vin)) throw new Error(`CSV 第 ${line} 行 VIN 重复:${vin}`);
@@ -28,7 +28,7 @@ export function parseVehicleProfileSyncCSV(text: string): VehicleProfileSyncItem
const runtimeSeconds = rawRuntime === '' ? null : Number(rawRuntime);
if (runtimeSeconds !== null && (!Number.isSafeInteger(runtimeSeconds) || runtimeSeconds < 0)) throw new Error(`CSV 第 ${line} 行累计运行秒数无效`);
return {
vin, modelName, vehicleType, companyName,
vin, brandName, modelName, vehicleType, companyName,
operationStatus: (operationStatus || 'unknown') as VehicleProfileSyncItem['operationStatus'],
accessProvider, firstAccessAt, runtimeSeconds
};

View File

@@ -6,6 +6,7 @@ describe('latest telemetry presentation', () => {
expect(formatTelemetryValue(42.567)).toBe('42.57');
expect(formatTelemetryValue(true)).toBe('是');
expect(formatTelemetryValue(null)).toBe('—');
expect(formatTelemetryValue(1, '启动1')).toBe('启动1');
});
it('translates server quality states', () => {

View File

@@ -1,6 +1,7 @@
import { formatZhNumber } from './formatters';
export function formatTelemetryValue(value: unknown) {
export function formatTelemetryValue(value: unknown, displayValue?: string) {
if (displayValue) return displayValue;
if (typeof value === 'number' && Number.isFinite(value)) {
return formatZhNumber(value, 2);
}