diff --git a/src/app/core/device-v2/device-v2-error.spec.ts b/src/app/core/device-v2/device-v2-error.spec.ts new file mode 100644 index 0000000..0baf3d4 --- /dev/null +++ b/src/app/core/device-v2/device-v2-error.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { DeviceV2RouteError } from '../protocol/device-v2/session'; +import { Bbp2ErrorCode } from '../protocol/device-v2/types'; +import { deviceV2ErrorMessage } from './device-v2-error'; + +describe('deviceV2ErrorMessage', () => { + it('keeps the wire error and appends an actionable Chinese explanation', () => { + const error = new DeviceV2RouteError(Bbp2ErrorCode.UnknownEndpoint); + + expect(deviceV2ErrorMessage(error, '设备同步失败')).toBe( + 'Device V2 route failed with wire error 5:未获取到设备能力,或设备当前未上线。', + ); + }); + + it('recognizes a serialized wire error message', () => { + const error = new Error('Device V2 route failed with wire error 12'); + + expect(deviceV2ErrorMessage(error, '指令发送失败')).toBe( + 'Device V2 route failed with wire error 12:操作过于频繁,请稍后再试。', + ); + }); + + it('keeps useful non-protocol error messages unchanged', () => { + expect(deviceV2ErrorMessage(new Error('connection lost'), '设备同步失败')) + .toBe('connection lost'); + }); + + it('uses the caller fallback for an unknown failure', () => { + expect(deviceV2ErrorMessage(undefined, '设备同步失败')).toBe('设备同步失败'); + }); +}); diff --git a/src/app/core/device-v2/device-v2-error.ts b/src/app/core/device-v2/device-v2-error.ts new file mode 100644 index 0000000..d085cd7 --- /dev/null +++ b/src/app/core/device-v2/device-v2-error.ts @@ -0,0 +1,37 @@ +import { DeviceV2RouteError } from '../protocol/device-v2/session'; +import { Bbp2ErrorCode } from '../protocol/device-v2/types'; + +const routeErrorExplanations: Partial> = { + [Bbp2ErrorCode.MalformedMessage]: '设备无法识别本次指令,请更新设备固件后重试。', + [Bbp2ErrorCode.AuthenticationRequired]: '设备连接鉴权已失效,请重新连接。', + [Bbp2ErrorCode.NegotiationRequired]: '设备能力尚未同步,请稍后重试。', + [Bbp2ErrorCode.UnsupportedMessage]: '当前设备不支持此操作。', + [Bbp2ErrorCode.UnknownEndpoint]: '未获取到设备能力,或设备当前未上线。', + [Bbp2ErrorCode.CommandRejected]: '设备拒绝执行本次操作。', + [Bbp2ErrorCode.ResourceExhausted]: '设备正忙,请稍后重试。', + [Bbp2ErrorCode.Internal]: '设备通信发生内部错误,请稍后重试。', + [Bbp2ErrorCode.SequenceConflict]: '设备数据正在同步,请稍后重试。', + [Bbp2ErrorCode.StateConflict]: '设备状态已变化,请刷新后重试。', + [Bbp2ErrorCode.ManifestConflict]: '设备能力已变化,正在重新同步。', + [Bbp2ErrorCode.RateLimited]: '操作过于频繁,请稍后再试。', +}; + +const wireErrorPattern = /Device V2 route failed with wire error (\d+)/; + +export function deviceV2ErrorMessage(error: unknown, fallback: string): string { + const original = error instanceof Error && error.message ? error.message : fallback; + const routeCode = deviceV2RouteErrorCode(error, original); + const explanation = routeCode === undefined ? undefined : routeErrorExplanations[routeCode]; + + return explanation ? `${original}:${explanation}` : original; +} + +function deviceV2RouteErrorCode(error: unknown, message: string): Bbp2ErrorCode | undefined { + if (error instanceof DeviceV2RouteError) return error.code; + + const match = wireErrorPattern.exec(message); + if (!match) return undefined; + + const code = Number(match[1]); + return Number.isInteger(code) ? code as Bbp2ErrorCode : undefined; +} diff --git a/src/app/device/v2/device-v2.page.html b/src/app/device/v2/device-v2.page.html index 3704d2a..a84fd3d 100644 --- a/src/app/device/v2/device-v2.page.html +++ b/src/app/device/v2/device-v2.page.html @@ -14,13 +14,6 @@
- @if (error) { -
- {{ error }} - 重试 -
- } - @if (telemetryError) {
实时数据已降级:{{ telemetryError }} @@ -127,5 +120,12 @@
} } + + @if (error) { + + }
diff --git a/src/app/device/v2/device-v2.page.scss b/src/app/device/v2/device-v2.page.scss index da31700..c8a5eaa 100644 --- a/src/app/device/v2/device-v2.page.scss +++ b/src/app/device/v2/device-v2.page.scss @@ -26,6 +26,8 @@ ion-content { .v2-content { box-sizing: border-box; + display: flex; + flex-direction: column; max-width: 720px; min-height: 100%; margin: 0 auto; @@ -56,11 +58,26 @@ ion-content { gap: 12px; } +.notice { + > span { + min-width: 0; + overflow-wrap: anywhere; + } + + ion-button { + flex: 0 0 auto; + } +} + .notice.error, .field-error { color: var(--ion-color-danger); } +.notice.error { + margin-top: auto; +} + .summary { color: var(--blinker-text-secondary); font-size: 12px; diff --git a/src/app/device/v2/device-v2.page.ts b/src/app/device/v2/device-v2.page.ts index a229094..be3577c 100644 --- a/src/app/device/v2/device-v2.page.ts +++ b/src/app/device/v2/device-v2.page.ts @@ -22,6 +22,7 @@ import { DeviceUiTelemetryLease, DeviceUiValue, } from '../../core/device-v2/device-ui.port'; +import { deviceV2ErrorMessage } from '../../core/device-v2/device-v2-error'; import { diffPageLayout, generateDefaultPageLayout, @@ -568,7 +569,7 @@ export class DeviceV2Page implements OnInit, OnChanges, OnDestroy { } private messageOf(error: unknown, fallback: string): string { - return error instanceof Error && error.message ? error.message : fallback; + return deviceV2ErrorMessage(error, fallback); } private errorCode(error: unknown): string {