@tx5dr/contracts 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/schema/__tests__/radio-power-support.test.d.ts +2 -0
  2. package/dist/schema/__tests__/radio-power-support.test.d.ts.map +1 -0
  3. package/dist/schema/__tests__/radio-power-support.test.js +19 -0
  4. package/dist/schema/__tests__/radio-power-support.test.js.map +1 -0
  5. package/dist/schema/__tests__/realtime-settings.schema.test.d.ts +2 -0
  6. package/dist/schema/__tests__/realtime-settings.schema.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/realtime-settings.schema.test.js +34 -0
  8. package/dist/schema/__tests__/realtime-settings.schema.test.js.map +1 -0
  9. package/dist/schema/cpu-profile.schema.d.ts +4 -4
  10. package/dist/schema/desktop-https.schema.d.ts +9 -0
  11. package/dist/schema/desktop-https.schema.d.ts.map +1 -1
  12. package/dist/schema/desktop-https.schema.js +3 -0
  13. package/dist/schema/desktop-https.schema.js.map +1 -1
  14. package/dist/schema/openwebrx.schema.d.ts +1 -1
  15. package/dist/schema/openwebrx.schema.d.ts.map +1 -1
  16. package/dist/schema/openwebrx.schema.js +1 -1
  17. package/dist/schema/openwebrx.schema.js.map +1 -1
  18. package/dist/schema/operator.schema.d.ts +32 -32
  19. package/dist/schema/plugin.schema.d.ts +670 -136
  20. package/dist/schema/plugin.schema.d.ts.map +1 -1
  21. package/dist/schema/plugin.schema.js +37 -2
  22. package/dist/schema/plugin.schema.js.map +1 -1
  23. package/dist/schema/radio-capability.schema.d.ts +42 -2
  24. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  25. package/dist/schema/radio-capability.schema.js +18 -1
  26. package/dist/schema/radio-capability.schema.js.map +1 -1
  27. package/dist/schema/radio-power-support.d.ts +8 -0
  28. package/dist/schema/radio-power-support.d.ts.map +1 -1
  29. package/dist/schema/radio-power-support.js +38 -26
  30. package/dist/schema/radio-power-support.js.map +1 -1
  31. package/dist/schema/radio-power.schema.d.ts +10 -6
  32. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  33. package/dist/schema/radio-power.schema.js +8 -6
  34. package/dist/schema/radio-power.schema.js.map +1 -1
  35. package/dist/schema/realtime.schema.d.ts +507 -417
  36. package/dist/schema/realtime.schema.d.ts.map +1 -1
  37. package/dist/schema/realtime.schema.js +85 -51
  38. package/dist/schema/realtime.schema.js.map +1 -1
  39. package/dist/schema/server-message-keys.d.ts +2 -2
  40. package/dist/schema/server-message-keys.d.ts.map +1 -1
  41. package/dist/schema/server-message-keys.js +2 -2
  42. package/dist/schema/server-message-keys.js.map +1 -1
  43. package/dist/schema/slot-info.schema.d.ts +6 -0
  44. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  45. package/dist/schema/slot-info.schema.js +3 -1
  46. package/dist/schema/slot-info.schema.js.map +1 -1
  47. package/dist/schema/websocket.schema.d.ts +1408 -612
  48. package/dist/schema/websocket.schema.d.ts.map +1 -1
  49. package/dist/schema/websocket.schema.js +2 -1
  50. package/dist/schema/websocket.schema.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/schema/__tests__/radio-power-support.test.ts +26 -0
  53. package/src/schema/__tests__/realtime-settings.schema.test.ts +40 -0
  54. package/src/schema/desktop-https.schema.ts +3 -1
  55. package/src/schema/openwebrx.schema.ts +1 -1
  56. package/src/schema/plugin.schema.ts +43 -2
  57. package/src/schema/radio-capability.schema.ts +25 -1
  58. package/src/schema/radio-power-support.ts +49 -26
  59. package/src/schema/radio-power.schema.ts +8 -6
  60. package/src/schema/realtime.schema.ts +96 -65
  61. package/src/schema/server-message-keys.ts +2 -2
  62. package/src/schema/slot-info.schema.ts +3 -1
  63. package/src/schema/websocket.schema.ts +3 -1
@@ -17,32 +17,40 @@ import type { HamlibConfig } from './radio.schema.js';
17
17
  export interface PowerCapableRigEntry {
18
18
  mfg: string | RegExp;
19
19
  model: string | RegExp;
20
+ /**
21
+ * Physical powerstat targets that are safe to expose while CAT is connected.
22
+ * `on` is represented by canPowerOn and is not part of this connected-state list.
23
+ */
24
+ supportedStates?: ReadonlyArray<'operate' | 'standby' | 'off'>;
20
25
  }
21
26
 
22
27
  export const POWER_CAPABLE_RIGS: ReadonlyArray<PowerCapableRigEntry> = [
23
28
  // Icom modern transceivers (CI-V 0x18 command)
24
- { mfg: /^icom$/i, model: /^IC-?705$/i },
25
- { mfg: /^icom$/i, model: /^IC-?7300$/i },
26
- { mfg: /^icom$/i, model: /^IC-?7610$/i },
27
- { mfg: /^icom$/i, model: /^IC-?7100$/i },
28
- { mfg: /^icom$/i, model: /^IC-?7851$/i },
29
- { mfg: /^icom$/i, model: /^IC-?9700$/i },
30
- { mfg: /^icom$/i, model: /^IC-?R8600$/i },
29
+ { mfg: /^icom$/i, model: /^IC-?705$/i, supportedStates: ['off', 'standby', 'operate'] },
30
+ { mfg: /^icom$/i, model: /^IC-?7300$/i, supportedStates: ['off', 'standby', 'operate'] },
31
+ { mfg: /^icom$/i, model: /^IC-?7610$/i, supportedStates: ['off', 'standby', 'operate'] },
32
+ { mfg: /^icom$/i, model: /^IC-?7100$/i, supportedStates: ['off', 'standby', 'operate'] },
33
+ { mfg: /^icom$/i, model: /^IC-?7851$/i, supportedStates: ['off', 'standby', 'operate'] },
34
+ { mfg: /^icom$/i, model: /^IC-?9700$/i, supportedStates: ['off', 'standby', 'operate'] },
35
+ { mfg: /^icom$/i, model: /^IC-?R8600$/i, supportedStates: ['off', 'standby', 'operate'] },
31
36
 
32
37
  // Kenwood
33
- { mfg: /^kenwood$/i, model: /^TS-590SG?$/i },
34
- { mfg: /^kenwood$/i, model: /^TS-890$/i },
35
- { mfg: /^kenwood$/i, model: /^TS-990$/i },
38
+ { mfg: /^kenwood$/i, model: /^TS-590SG?$/i, supportedStates: ['off', 'standby'] },
39
+ { mfg: /^kenwood$/i, model: /^TS-890$/i, supportedStates: ['off', 'standby'] },
40
+ { mfg: /^kenwood$/i, model: /^TS-990$/i, supportedStates: ['off', 'standby'] },
36
41
 
37
- // Yaesu
38
- { mfg: /^yaesu$/i, model: /^FT-991A?$/i },
39
- { mfg: /^yaesu$/i, model: /^FTDX-?10$/i },
40
- { mfg: /^yaesu$/i, model: /^FTDX-?101(MP|D)?$/i },
41
- { mfg: /^yaesu$/i, model: /^FT-?710$/i },
42
+ // Yaesu: FT-710 rejects powerstat(operate), so do not expose it by default.
43
+ { mfg: /^yaesu$/i, model: /^FT-991A?$/i, supportedStates: ['off', 'standby'] },
44
+ { mfg: /^yaesu$/i, model: /^FTDX-?10$/i, supportedStates: ['off', 'standby'] },
45
+ { mfg: /^yaesu$/i, model: /^FTDX-?101(MP|D)?$/i, supportedStates: ['off', 'standby'] },
46
+ { mfg: /^yaesu$/i, model: /^FT-?710$/i, supportedStates: ['off'] },
42
47
  ];
43
48
 
44
- export function isRigModelPowerCapable(mfgName: string, modelName: string): boolean {
45
- return POWER_CAPABLE_RIGS.some((entry) => {
49
+ export function findPowerCapableRigEntry(
50
+ mfgName: string,
51
+ modelName: string
52
+ ): PowerCapableRigEntry | undefined {
53
+ return POWER_CAPABLE_RIGS.find((entry) => {
46
54
  const mfgOk =
47
55
  typeof entry.mfg === 'string'
48
56
  ? entry.mfg.toLowerCase() === mfgName.toLowerCase()
@@ -55,6 +63,10 @@ export function isRigModelPowerCapable(mfgName: string, modelName: string): bool
55
63
  });
56
64
  }
57
65
 
66
+ export function isRigModelPowerCapable(mfgName: string, modelName: string): boolean {
67
+ return findPowerCapableRigEntry(mfgName, modelName) !== undefined;
68
+ }
69
+
58
70
  export type PowerSupportReason =
59
71
  | 'model-unsupported'
60
72
  | 'network-mode-no-wake'
@@ -65,6 +77,8 @@ export interface PowerSupportDecision {
65
77
  canPowerOn: boolean;
66
78
  /** Whether the UI should surface a "power off" control. */
67
79
  canPowerOff: boolean;
80
+ /** Connected-state physical power targets the UI may offer. */
81
+ supportedStates: Array<'operate' | 'standby' | 'off'>;
68
82
  /** Machine-readable reason when `canPowerOn` is false. */
69
83
  reason?: PowerSupportReason;
70
84
  }
@@ -85,23 +99,32 @@ export function decidePowerSupport(
85
99
  ): PowerSupportDecision {
86
100
  switch (config.type) {
87
101
  case 'none':
88
- return { canPowerOn: false, canPowerOff: false, reason: 'none-mode' };
102
+ return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'none-mode' };
89
103
  case 'network':
90
- return { canPowerOn: false, canPowerOff: true, reason: 'network-mode-no-wake' };
104
+ return {
105
+ canPowerOn: false,
106
+ canPowerOff: true,
107
+ supportedStates: ['off'],
108
+ reason: 'network-mode-no-wake',
109
+ };
91
110
  case 'icom-wlan':
92
111
  // ICOM WLAN 的 CI-V-over-UDP 通道在电台关机后无法维持;即便已连接
93
112
  // 发送 powerstat(off) 也不能可靠恢复。整体不暴露电源控制。
94
- return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
113
+ return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
95
114
  case 'serial': {
96
115
  if (!rigInfo) {
97
- return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
116
+ return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
98
117
  }
99
- const supported = isRigModelPowerCapable(rigInfo.mfgName, rigInfo.modelName);
100
- return supported
101
- ? { canPowerOn: true, canPowerOff: true }
102
- : { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
118
+ const entry = findPowerCapableRigEntry(rigInfo.mfgName, rigInfo.modelName);
119
+ return entry
120
+ ? {
121
+ canPowerOn: true,
122
+ canPowerOff: true,
123
+ supportedStates: [...(entry.supportedStates ?? ['off'])],
124
+ }
125
+ : { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
103
126
  }
104
127
  default:
105
- return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
128
+ return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
106
129
  }
107
130
  }
@@ -5,10 +5,10 @@ import { HamlibConfigSchema } from './radio.schema.js';
5
5
  * High-level runtime state tracked by the RadioPowerController.
6
6
  *
7
7
  * - `off`: radio is known to be off (or was never connected).
8
- * - `waking`: power-on command sent, waiting for the radio to respond.
9
- * - `awake`: radio is responding; the engine is being started (if autoEngine).
10
- * - `shutting_down`: engine is stopping in preparation for a power-off command.
11
- * - `entering_standby`: powerstat(2) sent, waiting for radio to drop CAT link.
8
+ * - `waking`: physical power-on command sent, waiting for the radio to respond.
9
+ * - `awake`: radio is physically responding; software engine startup is optional.
10
+ * - `shutting_down`: physical power-off command is being applied.
11
+ * - `entering_standby`: physical standby command is being applied.
12
12
  * - `failed`: last transition failed; UI should show an error + retry.
13
13
  */
14
14
  export const RadioPowerStateSchema = z.enum([
@@ -53,14 +53,15 @@ export type RadioPowerStateEvent = z.infer<typeof RadioPowerStateEventSchema>;
53
53
  /**
54
54
  * REST request body: POST /api/radio/power
55
55
  */
56
- /** Allowed target states for a power request. */
56
+ /** Allowed physical radio target states for a power request. */
57
57
  export const RadioPowerTargetSchema = z.enum(['on', 'off', 'standby', 'operate']);
58
58
  export type RadioPowerTarget = z.infer<typeof RadioPowerTargetSchema>;
59
59
 
60
60
  export const RadioPowerRequestSchema = z.object({
61
61
  profileId: z.string().min(1),
62
+ /** Physical radio power target. This is not the TX-5DR software engine state. */
62
63
  state: RadioPowerTargetSchema,
63
- /** Automatically start the engine after successful power-on. Defaults to true. */
64
+ /** Automatically start the TX-5DR engine after successful physical power-on. */
64
65
  autoEngine: z.boolean().optional().default(true),
65
66
  });
66
67
  export type RadioPowerRequest = z.infer<typeof RadioPowerRequestSchema>;
@@ -70,6 +71,7 @@ export type RadioPowerRequest = z.infer<typeof RadioPowerRequestSchema>;
70
71
  */
71
72
  export const RadioPowerResponseSchema = z.object({
72
73
  success: z.boolean(),
74
+ target: RadioPowerTargetSchema,
73
75
  state: RadioPowerStateSchema,
74
76
  });
75
77
  export type RadioPowerResponse = z.infer<typeof RadioPowerResponseSchema>;
@@ -3,10 +3,7 @@ import { z } from 'zod';
3
3
  export const RealtimeScopeSchema = z.enum(['radio', 'openwebrx-preview']);
4
4
  export type RealtimeScope = z.infer<typeof RealtimeScopeSchema>;
5
5
 
6
- export const RealtimeParticipantKindSchema = z.enum(['listener', 'publisher', 'bridge']);
7
- export type RealtimeParticipantKind = z.infer<typeof RealtimeParticipantKindSchema>;
8
-
9
- export const RealtimeTransportKindSchema = z.enum(['livekit', 'ws-compat']);
6
+ export const RealtimeTransportKindSchema = z.enum(['rtc-data-audio', 'ws-compat']);
10
7
  export type RealtimeTransportKind = z.infer<typeof RealtimeTransportKindSchema>;
11
8
 
12
9
  export const RealtimeSessionDirectionSchema = z.enum(['recv', 'send']);
@@ -27,10 +24,14 @@ export type RealtimeConnectivityErrorCode = z.infer<typeof RealtimeConnectivityE
27
24
 
28
25
  export const RealtimeConnectivityHintsSchema = z.object({
29
26
  signalingUrl: z.string(),
30
- signalingPort: z.number().int().positive(),
31
- rtcTcpPort: z.number().int().positive(),
32
- udpPortRange: z.string(),
33
- publicUrlOverrideActive: z.boolean(),
27
+ localUdpPort: z.number().int().min(1).max(65535),
28
+ publicCandidateEnabled: z.boolean(),
29
+ publicEndpoint: z.object({
30
+ host: z.string(),
31
+ port: z.number().int().min(1).max(65535),
32
+ }).nullable(),
33
+ iceServers: z.array(z.string()),
34
+ fallbackTransport: z.literal('ws-compat'),
34
35
  });
35
36
  export type RealtimeConnectivityHints = z.infer<typeof RealtimeConnectivityHintsSchema>;
36
37
 
@@ -45,14 +46,6 @@ export const RealtimeConnectivityIssueSchema = z.object({
45
46
  });
46
47
  export type RealtimeConnectivityIssue = z.infer<typeof RealtimeConnectivityIssueSchema>;
47
48
 
48
- export const RealtimeTokenRequestSchema = z.object({
49
- scope: RealtimeScopeSchema,
50
- publish: z.boolean().optional(),
51
- previewSessionId: z.string().optional(),
52
- });
53
-
54
- export type RealtimeTokenRequest = z.infer<typeof RealtimeTokenRequestSchema>;
55
-
56
49
  export const RealtimeSessionRequestSchema = z.object({
57
50
  scope: RealtimeScopeSchema,
58
51
  direction: RealtimeSessionDirectionSchema,
@@ -62,30 +55,6 @@ export const RealtimeSessionRequestSchema = z.object({
62
55
 
63
56
  export type RealtimeSessionRequest = z.infer<typeof RealtimeSessionRequestSchema>;
64
57
 
65
- export const RealtimeParticipantMetadataSchema = z.object({
66
- role: z.string(),
67
- tokenId: z.string().nullable().optional(),
68
- operatorIds: z.array(z.string()).optional(),
69
- clientKind: z.string(),
70
- participantKind: RealtimeParticipantKindSchema,
71
- scope: RealtimeScopeSchema,
72
- previewSessionId: z.string().optional(),
73
- });
74
-
75
- export type RealtimeParticipantMetadata = z.infer<typeof RealtimeParticipantMetadataSchema>;
76
-
77
- export const RealtimeTokenResponseSchema = z.object({
78
- url: z.string(),
79
- roomName: z.string(),
80
- token: z.string(),
81
- participantIdentity: z.string(),
82
- participantName: z.string(),
83
- participantMetadata: RealtimeParticipantMetadataSchema,
84
- connectivityHints: RealtimeConnectivityHintsSchema,
85
- });
86
-
87
- export type RealtimeTokenResponse = z.infer<typeof RealtimeTokenResponseSchema>;
88
-
89
58
  export const RealtimeTransportOfferSchema = z.object({
90
59
  transport: RealtimeTransportKindSchema,
91
60
  direction: RealtimeSessionDirectionSchema,
@@ -93,7 +62,6 @@ export const RealtimeTransportOfferSchema = z.object({
93
62
  token: z.string(),
94
63
  participantIdentity: z.string().nullable().optional(),
95
64
  participantName: z.string().nullable().optional(),
96
- roomName: z.string().nullable().optional(),
97
65
  });
98
66
 
99
67
  export type RealtimeTransportOffer = z.infer<typeof RealtimeTransportOfferSchema>;
@@ -101,8 +69,74 @@ export type RealtimeTransportOffer = z.infer<typeof RealtimeTransportOfferSchema
101
69
  export const RealtimeTransportPolicySchema = z.enum(['auto', 'force-compat']);
102
70
  export type RealtimeTransportPolicy = z.infer<typeof RealtimeTransportPolicySchema>;
103
71
 
104
- export const LiveKitNetworkModeSchema = z.enum(['lan', 'internet-auto', 'internet-manual']);
105
- export type LiveKitNetworkMode = z.infer<typeof LiveKitNetworkModeSchema>;
72
+ function isValidIpv4Literal(value: string): boolean {
73
+ const parts = value.split('.');
74
+ return parts.length === 4 && parts.every((part) => {
75
+ if (!/^\d{1,3}$/.test(part)) {
76
+ return false;
77
+ }
78
+ const parsed = Number.parseInt(part, 10);
79
+ return parsed >= 0 && parsed <= 255;
80
+ });
81
+ }
82
+
83
+ function isValidIpv6Literal(value: string): boolean {
84
+ if (!value.includes(':') || value.includes('[') || value.includes(']')) {
85
+ return false;
86
+ }
87
+ try {
88
+ // WHATWG URL parsing gives us a portable IPv6 sanity check without Node-only APIs.
89
+ // The schema still forbids bracket notation because the setting is host-only.
90
+ const parsed = new URL(`http://[${value}]/`);
91
+ return parsed.hostname.length > 2;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ function isValidDnsHostname(value: string): boolean {
98
+ if (value.length > 253 || value.endsWith('.')) {
99
+ return false;
100
+ }
101
+ const labels = value.split('.');
102
+ return labels.every((label) => (
103
+ label.length >= 1
104
+ && label.length <= 63
105
+ && /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)
106
+ ));
107
+ }
108
+
109
+ function isValidRtcDataAudioPublicHost(value: string): boolean {
110
+ const host = value.trim();
111
+ if (!host || /\s/.test(host) || /[/?#@]/.test(host) || /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(host)) {
112
+ return false;
113
+ }
114
+ return isValidIpv4Literal(host) || isValidIpv6Literal(host) || isValidDnsHostname(host);
115
+ }
116
+
117
+ export const RtcDataAudioPublicHostSchema = z.preprocess(
118
+ (value) => (typeof value === 'string' ? value.trim() : value),
119
+ z.union([
120
+ z.literal('').transform(() => null),
121
+ z.string().min(1).max(253).refine(isValidRtcDataAudioPublicHost, {
122
+ message: 'Public host must be an IP address or DNS hostname without scheme, port, or path',
123
+ }),
124
+ z.null(),
125
+ ]),
126
+ );
127
+ export type RtcDataAudioPublicHost = z.infer<typeof RtcDataAudioPublicHostSchema>;
128
+
129
+ export const RtcDataAudioPublicUdpPortSchema = z.preprocess((value) => {
130
+ if (value === '' || value === undefined) {
131
+ return null;
132
+ }
133
+ if (typeof value === 'string') {
134
+ const parsed = Number.parseInt(value, 10);
135
+ return Number.isFinite(parsed) ? parsed : value;
136
+ }
137
+ return value;
138
+ }, z.number().int().min(1).max(65535).nullable());
139
+ export type RtcDataAudioPublicUdpPort = z.infer<typeof RtcDataAudioPublicUdpPortSchema>;
106
140
 
107
141
  export const RealtimeSessionResponseSchema = z.object({
108
142
  scope: RealtimeScopeSchema,
@@ -112,10 +146,8 @@ export const RealtimeSessionResponseSchema = z.object({
112
146
  selectionReason: z.enum([
113
147
  'client-override',
114
148
  'server-policy',
115
- 'bridge-unhealthy',
116
- 'livekit-disabled',
117
- 'runtime-unavailable',
118
- 'default-livekit',
149
+ 'default-rtc-data-audio',
150
+ 'rtc-data-audio-unavailable',
119
151
  ]),
120
152
  forcedCompatibilityMode: z.boolean(),
121
153
  offers: z.array(RealtimeTransportOfferSchema).min(1),
@@ -125,34 +157,27 @@ export const RealtimeSessionResponseSchema = z.object({
125
157
  export type RealtimeSessionResponse = z.infer<typeof RealtimeSessionResponseSchema>;
126
158
 
127
159
  export const RealtimeSettingsSchema = z.object({
128
- publicWsUrl: z.string().url().nullable().optional(),
129
160
  transportPolicy: RealtimeTransportPolicySchema.optional(),
130
- networkMode: LiveKitNetworkModeSchema.optional(),
131
- nodeIp: z.string().nullable().optional(),
161
+ rtcDataAudioPublicHost: RtcDataAudioPublicHostSchema.optional(),
162
+ rtcDataAudioPublicUdpPort: RtcDataAudioPublicUdpPortSchema.optional(),
132
163
  });
133
164
 
134
165
  export type RealtimeSettings = z.infer<typeof RealtimeSettingsSchema>;
135
166
 
136
- export const RealtimeCredentialSourceSchema = z.enum(['managed-file', 'environment-override', 'missing']);
137
- export type RealtimeCredentialSource = z.infer<typeof RealtimeCredentialSourceSchema>;
138
-
139
- export const RealtimeCredentialStatusSchema = z.object({
140
- initialized: z.boolean(),
141
- source: RealtimeCredentialSourceSchema,
142
- filePath: z.string().nullable(),
143
- apiKeyPreview: z.string().nullable(),
144
- createdAt: z.string().nullable(),
145
- rotatedAt: z.string().nullable(),
167
+ export const RealtimeRtcDataAudioRuntimeSchema = z.object({
168
+ localUdpPort: z.number().int().min(1).max(65535),
169
+ publicCandidateEnabled: z.boolean(),
170
+ publicEndpoint: z.object({
171
+ host: z.string(),
172
+ port: z.number().int().min(1).max(65535),
173
+ }).nullable(),
146
174
  });
147
- export type RealtimeCredentialStatus = z.infer<typeof RealtimeCredentialStatusSchema>;
175
+ export type RealtimeRtcDataAudioRuntime = z.infer<typeof RealtimeRtcDataAudioRuntimeSchema>;
148
176
 
149
177
  export const RealtimeSettingsRuntimeSchema = z.object({
150
- liveKitEnabled: z.boolean(),
151
178
  connectivityHints: RealtimeConnectivityHintsSchema,
152
179
  radioReceiveTransport: RealtimeTransportKindSchema,
153
- radioBridgeHealthy: z.boolean(),
154
- radioBridgeIssueCode: RealtimeConnectivityErrorCodeSchema.nullable(),
155
- credentialStatus: RealtimeCredentialStatusSchema,
180
+ rtcDataAudio: RealtimeRtcDataAudioRuntimeSchema,
156
181
  });
157
182
 
158
183
  export type RealtimeSettingsRuntime = z.infer<typeof RealtimeSettingsRuntimeSchema>;
@@ -234,6 +259,10 @@ export const RealtimeVoiceTxServerIngressStatsSchema = z.object({
234
259
  queueDepthFrames: z.number().int().nonnegative(),
235
260
  queuedAudioMs: z.number().nonnegative(),
236
261
  droppedFrames: z.number().int().nonnegative(),
262
+ staleDroppedFrames: z.number().int().nonnegative(),
263
+ underrunCount: z.number().int().nonnegative(),
264
+ plcFrames: z.number().int().nonnegative(),
265
+ jitterTargetMs: z.number().nonnegative(),
237
266
  });
238
267
 
239
268
  export type RealtimeVoiceTxServerIngressStats = z.infer<typeof RealtimeVoiceTxServerIngressStatsSchema>;
@@ -242,8 +271,10 @@ export const RealtimeVoiceTxServerOutputStatsSchema = z.object({
242
271
  resampleMs: RealtimeVoiceTxMetricWindowSchema,
243
272
  queueWaitMs: RealtimeVoiceTxMetricWindowSchema,
244
273
  writeMs: RealtimeVoiceTxMetricWindowSchema,
274
+ serverPipelineMs: RealtimeVoiceTxMetricWindowSchema,
245
275
  endToEndMs: RealtimeVoiceTxMetricWindowSchema,
246
276
  outputBufferedMs: RealtimeVoiceTxMetricWindowSchema,
277
+ outputWriteIntervalMs: RealtimeVoiceTxMetricWindowSchema,
247
278
  outputSampleRate: z.number().nullable(),
248
279
  outputBufferSize: z.number().nullable(),
249
280
  writeFailures: z.number().int().nonnegative(),
@@ -18,8 +18,8 @@ export enum ServerMessageKey {
18
18
  /** 实时语音连接异常 */
19
19
  REALTIME_CONNECTIVITY_WARNING = 'realtimeConnectivityWarning',
20
20
 
21
- /** 服务端实时语音桥接异常 */
22
- REALTIME_BRIDGE_DOWN = 'realtimeBridgeDown',
21
+ /** 服务端实时语音服务异常 */
22
+ REALTIME_SERVICE_DOWN = 'realtimeServiceDown',
23
23
 
24
24
  /** 实时语音链路无音频数据 */
25
25
  REALTIME_NO_AUDIO = 'realtimeNoAudio',
@@ -148,7 +148,9 @@ export const SlotPackSchema = z.object({
148
148
  /** 去重后的帧数 */
149
149
  totalFramesAfterDedup: z.number().default(0),
150
150
  /** 最后更新时间戳 */
151
- lastUpdated: z.number().default(() => Date.now())
151
+ lastUpdated: z.number().default(() => Date.now()),
152
+ /** SlotPack 单调更新序号,用于客户端丢弃乱序旧包 */
153
+ updateSeq: z.number().int().nonnegative().optional()
152
154
  }).default({}),
153
155
  /** 解码历史(用于调试) */
154
156
  decodeHistory: z.array(z.object({
@@ -187,6 +187,7 @@ export enum WSMessageType {
187
187
  PLUGIN_USER_ACTION = 'pluginUserAction',
188
188
  PLUGIN_PAGE_PUSH = 'pluginPagePush',
189
189
  PLUGIN_PANEL_META = 'pluginPanelMeta',
190
+ PLUGIN_PANEL_CONTRIBUTIONS_CHANGED = 'pluginPanelContributionsChanged',
190
191
 
191
192
  // ===== OpenWebRX SDR =====
192
193
  OPENWEBRX_LISTEN_STATUS = 'openwebrxListenStatus',
@@ -216,7 +217,7 @@ export const SystemStatusSchema = z.object({
216
217
  /** 当前电台调制模式(语音模式下使用,如 USB/LSB/FM/AM) */
217
218
  currentRadioMode: z.string().optional(),
218
219
  /** 引擎状态机当前状态 */
219
- engineState: z.enum(['idle', 'waking', 'starting', 'running', 'stopping']).optional(),
220
+ engineState: z.enum(['idle', 'starting', 'running', 'stopping']).optional(),
220
221
  /** 引擎状态机上下文的精简投影 */
221
222
  engineContext: z.object({
222
223
  error: z.string().optional(),
@@ -1490,6 +1491,7 @@ export interface DigitalRadioEngineEvents {
1490
1491
  data?: unknown;
1491
1492
  }) => void;
1492
1493
  pluginPanelMeta: (data: import('./plugin.schema.js').PluginPanelMetaPayload) => void;
1494
+ pluginPanelContributionsChanged: (data: import('./plugin.schema.js').PluginUIPanelContributionGroup) => void;
1493
1495
 
1494
1496
  // 内部子系统间通信事件(不通过 WebSocket 广播,用于 engine emitter 内部编排)
1495
1497
  encodeStart: (slotInfo: z.infer<typeof SlotInfoSchema>) => void;