262 lines
8.5 KiB
TypeScript
262 lines
8.5 KiB
TypeScript
import type { PluginMeasurement } from './plugins/types.js';
|
||
|
||
export type InspectionObjectKind = 'head' | 'middle' | 'unknown';
|
||
|
||
export interface InspectionObjectContext {
|
||
inspectionObject: InspectionObjectKind;
|
||
inspectionObjectName?: string;
|
||
}
|
||
|
||
export interface CompatibilityWarning {
|
||
code: 'legacy-unmapped';
|
||
pluginId: string;
|
||
originalName: string;
|
||
inspectionObject: InspectionObjectKind;
|
||
inspectionObjectName?: string;
|
||
}
|
||
|
||
export interface NormalizeMeasurementsInput {
|
||
pluginId?: string | null;
|
||
measurements?: unknown;
|
||
extra?: unknown;
|
||
context?: InspectionObjectContext;
|
||
algorithmProtocolVersion?: string | number | null;
|
||
}
|
||
|
||
export interface NormalizeMeasurementsOutput {
|
||
measurements: PluginMeasurement[];
|
||
extra: Record<string, unknown>;
|
||
warnings: CompatibilityWarning[];
|
||
converted: boolean;
|
||
}
|
||
|
||
const COUPLER_INTERMEDIATE_NAMES = new Set([
|
||
'最大 x 偏差 dx_y',
|
||
'最大 x 偏差 dx_z',
|
||
'面板 y 向最大偏差 D_y',
|
||
'面板 z 向最大偏差 D_z',
|
||
]);
|
||
|
||
const DEFLECTION_NAMES = new Map<string, string>([
|
||
['左梁 · 纵向梁到直线的垂直偏差(挠度)', '左梁'],
|
||
['右梁 · 纵向梁到直线的垂直偏差(挠度)', '右梁'],
|
||
['挠度左梁', '左梁'],
|
||
['挠度右梁', '右梁'],
|
||
]);
|
||
|
||
const COUPLER_NAMES = {
|
||
longitudinal: new Set([
|
||
'车钩面板距纵向中心的右偏转角',
|
||
'车钩面板距纵向中心的右偏转角(头车一位端处车钩)',
|
||
'车钩面板距纵向中心的右偏转角(头车一位端处车钩)',
|
||
]),
|
||
reference: new Set([
|
||
'车钩面板距参考面的右偏转角',
|
||
'车钩面板距参考面的右偏转角(头车一位端处车钩)',
|
||
]),
|
||
};
|
||
|
||
function asRecord(value: unknown): Record<string, unknown> {
|
||
return value && typeof value === 'object' && !Array.isArray(value)
|
||
? { ...(value as Record<string, unknown>) }
|
||
: {};
|
||
}
|
||
|
||
function pluginKey(pluginId: string): string {
|
||
return (pluginId || 'unknown').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '').toLowerCase() || 'unknown';
|
||
}
|
||
|
||
function stableNameHash(value: string): string {
|
||
let hash = 0x811c9dc5;
|
||
for (let i = 0; i < value.length; i += 1) {
|
||
hash ^= value.charCodeAt(i);
|
||
hash = Math.imul(hash, 0x01000193);
|
||
}
|
||
return (hash >>> 0).toString(16).padStart(8, '0');
|
||
}
|
||
|
||
function legacyMetricCode(pluginId: string, taskNo: string): string {
|
||
return `legacy.${pluginKey(pluginId)}.task_${taskNo}`;
|
||
}
|
||
|
||
function unmappedMetricCode(pluginId: string, name: string): string {
|
||
return `legacy.${pluginKey(pluginId)}.unmapped.${stableNameHash(name)}`;
|
||
}
|
||
|
||
export function inspectionObjectContextFromName(name?: string | null): InspectionObjectContext {
|
||
const normalized = name?.trim() ?? '';
|
||
return {
|
||
inspectionObject: normalized === '头车'
|
||
? 'head'
|
||
: normalized === '中车' || normalized === '中间车'
|
||
? 'middle'
|
||
: 'unknown',
|
||
inspectionObjectName: normalized || undefined,
|
||
};
|
||
}
|
||
|
||
export function measurementCompatibilityEnabled(): boolean {
|
||
return process.env.MEASUREMENT_COMPAT_ENABLED !== '0';
|
||
}
|
||
|
||
function positionOrder(position: unknown): number {
|
||
if (position === '左梁') return 0;
|
||
if (position === '右梁') return 1;
|
||
return 2;
|
||
}
|
||
|
||
export function sortNormalizedMeasurements<T extends { taskNo?: unknown; position?: unknown }>(items: readonly T[]): T[] {
|
||
return items
|
||
.map((item, index) => ({ item, index }))
|
||
.sort((a, b) => {
|
||
const aNo = typeof a.item.taskNo === 'string' && /^\d+$/.test(a.item.taskNo) ? Number(a.item.taskNo) : Number.POSITIVE_INFINITY;
|
||
const bNo = typeof b.item.taskNo === 'string' && /^\d+$/.test(b.item.taskNo) ? Number(b.item.taskNo) : Number.POSITIVE_INFINITY;
|
||
if (aNo !== bNo) return aNo - bNo;
|
||
const positionDelta = positionOrder(a.item.position) - positionOrder(b.item.position);
|
||
return positionDelta || a.index - b.index;
|
||
})
|
||
.map(({ item }) => item);
|
||
}
|
||
|
||
export function normalizeMeasurements(input: NormalizeMeasurementsInput): NormalizeMeasurementsOutput {
|
||
const pluginId = input.pluginId ?? '';
|
||
const context = input.context ?? { inspectionObject: 'unknown' as const };
|
||
const sourceMeasurements = Array.isArray(input.measurements)
|
||
? input.measurements.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
|
||
: [];
|
||
const sourceExtra = asRecord(input.extra);
|
||
|
||
if (!measurementCompatibilityEnabled()) {
|
||
return {
|
||
measurements: sourceMeasurements as unknown as PluginMeasurement[],
|
||
extra: sourceExtra,
|
||
warnings: [],
|
||
converted: false,
|
||
};
|
||
}
|
||
|
||
const measurements: PluginMeasurement[] = [];
|
||
const intermediateMetrics = Array.isArray(sourceExtra.intermediateMetrics)
|
||
? [...sourceExtra.intermediateMetrics]
|
||
: [];
|
||
const warnings: CompatibilityWarning[] = [];
|
||
let converted = false;
|
||
|
||
sourceMeasurements.forEach((source) => {
|
||
const item = { ...source };
|
||
const originalName = typeof item.name === 'string' ? item.name : '';
|
||
|
||
// 新协议项必须同时具备 taskNo 和 metricCode;重复规范化时保持原样。
|
||
if (typeof item.taskNo === 'string' && item.taskNo && typeof item.metricCode === 'string' && item.metricCode) {
|
||
measurements.push(item as unknown as PluginMeasurement);
|
||
return;
|
||
}
|
||
|
||
if (pluginId === 'coupler-panel' && COUPLER_INTERMEDIATE_NAMES.has(originalName)) {
|
||
intermediateMetrics.push(item);
|
||
converted = true;
|
||
return;
|
||
}
|
||
|
||
const generic = /^任务(\d+)(?:_|\s*·\s*)(.+)$/.exec(originalName);
|
||
if (generic) {
|
||
const [, taskNo, name] = generic;
|
||
measurements.push({
|
||
...item,
|
||
taskNo,
|
||
metricCode: legacyMetricCode(pluginId, taskNo),
|
||
name: name.trim(),
|
||
legacyOriginalName: originalName,
|
||
} as unknown as PluginMeasurement);
|
||
converted = true;
|
||
return;
|
||
}
|
||
|
||
if (pluginId === 'underframe-deflection') {
|
||
const position = DEFLECTION_NAMES.get(originalName);
|
||
if (position) {
|
||
measurements.push({
|
||
...item,
|
||
taskNo: '5',
|
||
metricCode: legacyMetricCode(pluginId, '5'),
|
||
name: '挠度',
|
||
position,
|
||
legacyOriginalName: originalName,
|
||
} as unknown as PluginMeasurement);
|
||
converted = true;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (pluginId === 'coupler-panel') {
|
||
const isLongitudinal = COUPLER_NAMES.longitudinal.has(originalName);
|
||
const isReference = COUPLER_NAMES.reference.has(originalName);
|
||
if (isLongitudinal || isReference) {
|
||
const taskNo = context.inspectionObject === 'head'
|
||
? (isLongitudinal ? '10' : '8')
|
||
: context.inspectionObject === 'middle'
|
||
? (isLongitudinal ? '12' : '9')
|
||
: undefined;
|
||
if (taskNo) {
|
||
measurements.push({
|
||
...item,
|
||
taskNo,
|
||
metricCode: legacyMetricCode(pluginId, taskNo),
|
||
name: isLongitudinal ? '纵向中心右偏转' : '参考面右偏转',
|
||
legacyOriginalName: originalName,
|
||
} as unknown as PluginMeasurement);
|
||
converted = true;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
const unmapped = {
|
||
...item,
|
||
metricCode: unmappedMetricCode(pluginId, originalName),
|
||
legacyUnmapped: true,
|
||
legacyOriginalName: originalName,
|
||
} as unknown as PluginMeasurement;
|
||
measurements.push(unmapped);
|
||
warnings.push({
|
||
code: 'legacy-unmapped',
|
||
pluginId,
|
||
originalName,
|
||
inspectionObject: context.inspectionObject,
|
||
inspectionObjectName: context.inspectionObjectName,
|
||
});
|
||
converted = true;
|
||
});
|
||
|
||
return {
|
||
measurements: sortNormalizedMeasurements(measurements),
|
||
extra: intermediateMetrics.length > 0 ? { ...sourceExtra, intermediateMetrics } : sourceExtra,
|
||
warnings,
|
||
converted,
|
||
};
|
||
}
|
||
|
||
export function logCompatibilityWarnings(resultId: number, warnings: readonly CompatibilityWarning[]): void {
|
||
warnings.forEach((warning) => {
|
||
const signature = [
|
||
resultId,
|
||
warning.pluginId,
|
||
warning.originalName,
|
||
warning.inspectionObject,
|
||
warning.inspectionObjectName ?? '',
|
||
].join('\u0000');
|
||
if (loggedWarningSignatures.has(signature)) return;
|
||
loggedWarningSignatures.add(signature);
|
||
console.warn('[MeasurementCompatibility] legacy measurement unmapped', {
|
||
resultId,
|
||
pluginId: warning.pluginId,
|
||
originalName: warning.originalName,
|
||
inspectionObject: warning.inspectionObject,
|
||
inspectionObjectName: warning.inspectionObjectName,
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 避免历史结果轮询每次重复打印同一条告警;进程重启后会重新记录一次。 */
|
||
const loggedWarningSignatures = new Set<string>();
|