feat: expand vehicle data platform capabilities

This commit is contained in:
lingniu
2026-07-27 16:46:15 +08:00
parent e3a1f80f86
commit 3c4bece72c
650 changed files with 62155 additions and 2552 deletions
@@ -5,15 +5,23 @@ import type {
AccessSummary,
AccessThresholdConfig,
AccessThresholdUpdate,
AccessIdentityClaimInput,
AccessIdentityClaimResult,
AccessUnresolvedIdentity,
AccessUnresolvedIdentityQuery,
AccessVehicleRow,
AlertAction,
AlertEvent,
AlertNotification,
AlertNotificationConfig,
AlertNotificationDeliveryHealth,
AlertNotificationRetryAudit,
AlertNotificationRetryResult,
AlertQuery,
AlertRule,
AlertRulePage,
AlertRuleInput,
AlertRuleRevision,
AlertSummary,
DailyMileageRow,
DashboardSummary,
@@ -21,7 +29,16 @@ import type {
HistoryDataResponse,
HistorySeriesResponse,
HistoryExportJob,
HistoryExportBatchAction, HistoryExportBatchResult,
HistoryExportCleanupPreview,
HistoryExportCleanupRecord,
HistoryExportCleanupResult,
HistoryExportCleanupAutomationState,
HistoryExportCleanupAutomationPolicyInput,
HistoryExportCleanupAutomationActionInput,
HistoryExportPage,
HistoryExportRequest,
HistoryPreferences,
HistoryMetricCatalog,
MetricCatalog,
LatestTelemetryResponse,
@@ -39,18 +56,29 @@ import type {
QualitySummary,
QualityIssueRow,
ReconciliationIssue,
ReconciliationAssignee,
ReconciliationAssignmentRequest,
ReconciliationBatchAssignmentRequest,
ReconciliationBatchActionRequest,
ReconciliationBatchActionResult,
ReconciliationQuery,
ReconciliationLifecycleRequest,
ReconciliationBatchLifecycleRequest,
ReconciliationSummary,
RawFrameRow,
RealtimeLocationRow,
SourceReadinessPlan,
SessionInfo,
LoginResponse,
OneOSExchangeResponse,
CustomerUserInput,
CustomerUserBatchItem,
CustomerUserBatchResult,
TrackPlaybackResponse,
VehicleRealtimeRow,
VehicleCoverageRow,
VehicleCoverageSummary,
VehicleBusinessFilters,
VehicleDetail,
VehicleProfile,
VehicleProfileInput,
@@ -85,6 +113,19 @@ export type VehicleOverviewBatchQuery = {
offset?: number;
};
export type MileageQuery = {
dateFrom: string;
dateTo: string;
keyword?: string;
vins?: string[];
vehicleScope?: 'bound';
protocol?: string;
protocols?: string[];
deduplicate?: boolean;
limit?: number;
offset?: number;
};
type ApiErrorEnvelope = {
error?: {
code?: string;
@@ -146,16 +187,18 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
}
}
async function requestBlob(path: string, signal?: AbortSignal) {
async function requestBlob(path: string, signal?: AbortSignal, requestInit?: RequestInit, fallbackFilename = 'history-export.csv') {
const token = getAccessToken();
const init: RequestInit = token ? { signal, headers: withAuthorization(undefined, token) } : { signal };
const init: RequestInit = token
? { ...requestInit, signal, headers: withAuthorization(requestInit?.headers, token) }
: { ...requestInit, signal };
const response = await fetch(path, init);
if (!response.ok) {
if (response.status === 401 && token) notifyUnauthorizedSession(token);
throw new Error(await responseErrorMessage(response));
}
const encodedFilename = response.headers.get('X-Export-Name')?.trim();
let filename = 'history-export.csv';
let filename = fallbackFilename;
if (encodedFilename) {
try {
filename = decodeURIComponent(encodedFilename);
@@ -221,6 +264,9 @@ export const api = {
login: (credentials: { username: string; password: string }) => request<LoginResponse>('/api/v2/auth/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials)
}),
exchangeOneOSTicket: (ticket: string) => request<OneOSExchangeResponse>('/api/v2/auth/oneos/exchange', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticket })
}),
logout: () => request<{ loggedOut: boolean }>('/api/v2/auth/logout', { method: 'POST' }),
changePassword: (input: { currentPassword: string; newPassword: string }) => request<{ changed: boolean }>('/api/v2/auth/password', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
@@ -229,6 +275,9 @@ export const api = {
createCustomerUser: (input: CustomerUserInput) => request<{ id: number }>('/api/v2/admin/users', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
batchCustomerUsers: (mode: 'preview' | 'create', items: CustomerUserBatchItem[]) => request<CustomerUserBatchResult>('/api/v2/admin/users/batch', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode, items })
}),
updateCustomerUser: (id: number, input: CustomerUserInput) => request<{ id: number }>(`/api/v2/admin/users/${id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
@@ -241,9 +290,42 @@ export const api = {
historyData: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistoryDataResponse>(`/api/v2/history/query?${params.toString()}`, signal ? { signal } : undefined),
historySeries: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistorySeriesResponse>(`/api/v2/history/series?${params.toString()}`, signal ? { signal } : undefined),
historyExports: (signal?: AbortSignal) => request<HistoryExportJob[]>('/api/v2/exports', withSignal(undefined, signal)),
historyExportPage: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistoryExportPage>(`/api/v2/exports/page?${params.toString()}`, withSignal(undefined, signal)),
historyExportCleanupPreview: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistoryExportCleanupPreview>(`/api/v2/exports/cleanup/preview?${params.toString()}`, withSignal(undefined, signal)),
historyExportCleanupAudit: (signal?: AbortSignal) => request<HistoryExportCleanupRecord[]>('/api/v2/exports/cleanup/audit?limit=20', withSignal(undefined, signal)),
cleanupHistoryExports: (input: { olderThanDays: number; ownerScope: 'all' | 'mine'; previewToken: string }) => request<HistoryExportCleanupResult>('/api/v2/exports/cleanup', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
historyExportCleanupAutomation: (signal?: AbortSignal) => request<HistoryExportCleanupAutomationState>('/api/v2/exports/cleanup/automation', withSignal(undefined, signal)),
updateHistoryExportCleanupAutomation: (input: HistoryExportCleanupAutomationPolicyInput) => request<HistoryExportCleanupAutomationState>('/api/v2/exports/cleanup/automation', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
approveHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request<HistoryExportCleanupAutomationState>(`/api/v2/exports/cleanup/automation/${encodeURIComponent(id)}/approve`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
rejectHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request<HistoryExportCleanupAutomationState>(`/api/v2/exports/cleanup/automation/${encodeURIComponent(id)}/reject`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
retryHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request<HistoryExportCleanupAutomationState>(`/api/v2/exports/cleanup/automation/${encodeURIComponent(id)}/retry`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
updateHistoryExportCleanupProtection: (id: string, input: { protected: boolean; reason: string }) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/cleanup-protection`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
historyPreferences: (signal?: AbortSignal) => request<HistoryPreferences>('/api/v2/history/preferences', withSignal(undefined, signal)),
updateHistoryPreferences: (input: Pick<HistoryPreferences, 'retentionDays' | 'fieldViews'>) => request<HistoryPreferences>('/api/v2/history/preferences', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
createHistoryExport: (query: HistoryExportRequest) => request<HistoryExportJob>('/api/v2/exports', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
cancelHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/cancel`, { method: 'POST' }),
rebuildHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/rebuild`, { method: 'POST' }),
archiveHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/archive`, { method: 'POST' }),
restoreHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/restore`, { method: 'POST' }),
batchHistoryExports: (input: { ids: string[]; action: HistoryExportBatchAction }) => request<HistoryExportBatchResult>('/api/v2/exports/batch', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
downloadHistoryExport: (id: string, signal?: AbortSignal) => requestBlob(`/api/v2/exports/${encodeURIComponent(id)}/download`, signal),
accessSummary: (query: AccessQuery, signal?: AbortSignal) => request<AccessSummary>('/api/v2/access/summary', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
@@ -254,6 +336,9 @@ export const api = {
accessUnresolvedIdentities: (query: AccessUnresolvedIdentityQuery, signal?: AbortSignal) => request<Page<AccessUnresolvedIdentity>>('/api/v2/access/unresolved-identities', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}, signal)),
claimAccessIdentity: (id: string, input: AccessIdentityClaimInput) => request<AccessIdentityClaimResult>(`/api/v2/access/unresolved-identities/${encodeURIComponent(id)}/claim`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
accessThresholds: (signal?: AbortSignal) => request<AccessThresholdConfig>('/api/v2/access/thresholds', withSignal(undefined, signal)),
updateAccessThresholds: (update: AccessThresholdUpdate) => request<AccessThresholdConfig>('/api/v2/access/thresholds', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(update)
@@ -269,21 +354,39 @@ export const api = {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(action)
}),
alertRulesV2: (signal?: AbortSignal) => request<AlertRule[]>('/api/v2/alerts/rules', withSignal(undefined, signal)),
alertRuleLibraryV2: (params = new URLSearchParams(), signal?: AbortSignal) => request<AlertRulePage>(`/api/v2/alerts/rules/library?${params.toString()}`, withSignal(undefined, signal)),
alertRuleRevisionsV2: (id: string, signal?: AbortSignal) => request<AlertRuleRevision[]>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/revisions`, withSignal(undefined, signal)),
saveAlertRuleV2: (input: AlertRuleInput) => request<AlertRule>(input.version > 0 ? `/api/v2/alerts/rules/${encodeURIComponent(input.id)}` : '/api/v2/alerts/rules', {
method: input.version > 0 ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
setAlertRuleEnabledV2: (id: string, update: { version: number; enabled: boolean; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/enabled`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(update)
}),
rollbackAlertRuleV2: (id: string, input: { targetVersion: number; currentVersion: number; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/rollback`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
archiveAlertRuleV2: (id: string, input: { version: number; reason: string; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/archive`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
restoreAlertRuleV2: (id: string, input: { version: number; reason: string; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/restore`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
alertNotificationConfigV2: (signal?: AbortSignal) => request<AlertNotificationConfig>('/api/v2/alerts/notification-config', withSignal(undefined, signal)),
alertNotificationDeliveryHealthV2: (signal?: AbortSignal) => request<AlertNotificationDeliveryHealth>('/api/v2/alerts/notifications/health', withSignal(undefined, signal)),
alertNotificationsV2: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<AlertNotification>>(`/api/v2/alerts/notifications?${params.toString()}`, withSignal(undefined, signal)),
readAlertNotificationsV2: (ids: number[]) => request<{ updated: number }>('/api/v2/alerts/notifications/read', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids })
}),
retryAlertNotificationV2: (id: number, input: { expectedAttemptCount: number; reason: string; idempotencyKey: string }) => request<AlertNotificationRetryResult>(`/api/v2/alerts/notifications/${encodeURIComponent(id)}/retry`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
alertNotificationRetryAuditsV2: (id: number, signal?: AbortSignal) => request<AlertNotificationRetryAudit[]>(`/api/v2/alerts/notifications/${encodeURIComponent(id)}/retry-audit`, withSignal(undefined, signal)),
dashboardSummary: () => request<DashboardSummary>('/api/dashboard/summary'),
vehicles: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`, signal ? { signal } : undefined),
vehicleResolve: (params = new URLSearchParams()) => request<VehicleIdentityResolution>(`/api/vehicles/resolve?${params.toString()}`),
vehicleCoverage: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<VehicleCoverageRow>>(`/api/vehicles/coverage?${params.toString()}`, signal ? { signal } : undefined),
vehicleCoverageSummary: (params = new URLSearchParams()) => request<VehicleCoverageSummary>(`/api/vehicles/coverage/summary?${params.toString()}`),
vehicleBusinessFilters: (signal?: AbortSignal) => request<VehicleBusinessFilters>('/api/vehicles/business-filters', withSignal(undefined, signal)),
vehicleDetail: (params = new URLSearchParams(), signal?: AbortSignal) => request<VehicleDetail>(`/api/vehicle-service?${params.toString()}`, withSignal(undefined, signal)),
vehicleProfile: (vin: string) => request<VehicleProfile>(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`),
latestTelemetry: (vin: string, signal?: AbortSignal) => request<LatestTelemetryResponse>(`/api/v2/vehicles/${encodeURIComponent(vin)}/telemetry/latest`, withSignal(undefined, signal)),
@@ -320,6 +423,16 @@ export const api = {
'/api/v2/reconciliation/issues',
withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)
),
reconciliationAssignees: (search = '', signal?: AbortSignal) => request<ReconciliationAssignee[]>(
`/api/v2/reconciliation/assignees?${new URLSearchParams(search ? { search } : {}).toString()}`,
withSignal(undefined, signal)
),
downloadReconciliationIssues: (query: ReconciliationQuery, signal?: AbortSignal) => requestBlob(
'/api/v2/reconciliation/issues/export',
signal,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) },
'quality-issues.csv'
),
reconciliationIssue: (id: string, signal?: AbortSignal) => request<ReconciliationIssue>(
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}`,
withSignal(undefined, signal)
@@ -328,12 +441,32 @@ export const api = {
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}/actions`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
batchUpdateReconciliationIssues: (input: ReconciliationBatchActionRequest) => request<ReconciliationBatchActionResult>(
'/api/v2/reconciliation/issues/batch-actions',
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
assignReconciliationIssue: (id: string, input: ReconciliationAssignmentRequest) => request<ReconciliationIssue>(
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}/assignment`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
batchAssignReconciliationIssues: (input: ReconciliationBatchAssignmentRequest) => request<ReconciliationBatchActionResult>(
'/api/v2/reconciliation/issues/batch-assignments',
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
setReconciliationIssueArchived: (id: string, archived: boolean, input: ReconciliationLifecycleRequest) => request<ReconciliationIssue>(
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}/${archived ? 'archive' : 'restore'}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
batchSetReconciliationIssuesArchived: (archived: boolean, input: ReconciliationBatchLifecycleRequest) => request<ReconciliationBatchActionResult>(
`/api/v2/reconciliation/issues/${archived ? 'batch-archive' : 'batch-restore'}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
updateVehicleProfile: (vin: string, input: VehicleProfileInput) => request<VehicleProfile>(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
syncVehicleProfiles: (input: VehicleProfileSyncRequest) => request<VehicleProfileSyncResult>('/api/v2/vehicle-profiles/sync', {
syncVehicleProfiles: (input: VehicleProfileSyncRequest, signal?: AbortSignal) => request<VehicleProfileSyncResult>('/api/v2/vehicle-profiles/sync', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
}, signal)),
vehicleServiceSummary: () => request<VehicleServiceSummary>('/api/vehicle-service/summary'),
vehicleServiceOverview: (params = new URLSearchParams()) => request<VehicleServiceOverview>(`/api/vehicle-service/overview?${params.toString()}`),
vehicleServiceOverviews: (query: VehicleOverviewBatchQuery) => request<Page<VehicleServiceOverview>>('/api/vehicle-service/overviews', {
@@ -351,8 +484,12 @@ export const api = {
body: JSON.stringify(query)
}),
mileageSummary: (params = new URLSearchParams()) => request<MileageSummary>(`/api/mileage/summary?${params.toString()}`),
dailyMileage: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<DailyMileageRow>>(`/api/mileage/daily?${params.toString()}`, signal ? { signal } : undefined),
mileageStatistics: (params = new URLSearchParams(), signal?: AbortSignal) => request<MileageStatistics>(`/api/v2/statistics/mileage?${params.toString()}`, signal ? { signal } : undefined),
dailyMileage: (query: MileageQuery, signal?: AbortSignal) => request<Page<DailyMileageRow>>('/api/mileage/daily', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}, signal)),
mileageStatistics: (query: MileageQuery, signal?: AbortSignal) => request<MileageStatistics>('/api/v2/statistics/mileage', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}, signal)),
onlineStatisticsSummary: (params = new URLSearchParams()) => request<OnlineStatisticsSummary>(`/api/statistics/online-summary?${params.toString()}`),
onlineVehicleStatuses: (params = new URLSearchParams()) => request<Page<OnlineVehicleStatusRow>>(`/api/statistics/online-vehicles?${params.toString()}`),
qualitySummary: (params = new URLSearchParams()) => request<QualitySummary>(`/api/quality/summary?${params.toString()}`),