@tx5dr/contracts 1.7.2 → 1.7.4

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 (142) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/ability.test.d.ts +2 -0
  6. package/dist/schema/__tests__/ability.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/ability.test.js +32 -0
  8. package/dist/schema/__tests__/ability.test.js.map +1 -0
  9. package/dist/schema/__tests__/audio.schema.test.js +4 -0
  10. package/dist/schema/__tests__/audio.schema.test.js.map +1 -1
  11. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts +2 -0
  12. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts.map +1 -0
  13. package/dist/schema/__tests__/cw-decoder.schema.test.js +114 -0
  14. package/dist/schema/__tests__/cw-decoder.schema.test.js.map +1 -0
  15. package/dist/schema/__tests__/device-ui.schema.test.d.ts +2 -0
  16. package/dist/schema/__tests__/device-ui.schema.test.d.ts.map +1 -0
  17. package/dist/schema/__tests__/device-ui.schema.test.js +111 -0
  18. package/dist/schema/__tests__/device-ui.schema.test.js.map +1 -0
  19. package/dist/schema/__tests__/operator.schema.test.d.ts +2 -0
  20. package/dist/schema/__tests__/operator.schema.test.d.ts.map +1 -0
  21. package/dist/schema/__tests__/operator.schema.test.js +23 -0
  22. package/dist/schema/__tests__/operator.schema.test.js.map +1 -0
  23. package/dist/schema/__tests__/process-monitor.schema.test.d.ts +2 -0
  24. package/dist/schema/__tests__/process-monitor.schema.test.d.ts.map +1 -0
  25. package/dist/schema/__tests__/process-monitor.schema.test.js +235 -0
  26. package/dist/schema/__tests__/process-monitor.schema.test.js.map +1 -0
  27. package/dist/schema/__tests__/radio.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/radio.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/radio.schema.test.js +143 -0
  30. package/dist/schema/__tests__/radio.schema.test.js.map +1 -0
  31. package/dist/schema/ability.d.ts +3 -1
  32. package/dist/schema/ability.d.ts.map +1 -1
  33. package/dist/schema/ability.js +11 -0
  34. package/dist/schema/ability.js.map +1 -1
  35. package/dist/schema/api-response.schema.d.ts +170 -3
  36. package/dist/schema/api-response.schema.d.ts.map +1 -1
  37. package/dist/schema/api-response.schema.js +22 -0
  38. package/dist/schema/api-response.schema.js.map +1 -1
  39. package/dist/schema/audio.schema.d.ts +369 -0
  40. package/dist/schema/audio.schema.d.ts.map +1 -1
  41. package/dist/schema/audio.schema.js +3 -0
  42. package/dist/schema/audio.schema.js.map +1 -1
  43. package/dist/schema/cpu-profile.schema.d.ts +6 -6
  44. package/dist/schema/cw-decoder.schema.d.ts +2501 -0
  45. package/dist/schema/cw-decoder.schema.d.ts.map +1 -0
  46. package/dist/schema/cw-decoder.schema.js +196 -0
  47. package/dist/schema/cw-decoder.schema.js.map +1 -0
  48. package/dist/schema/cw-keyer.schema.d.ts +368 -0
  49. package/dist/schema/cw-keyer.schema.d.ts.map +1 -0
  50. package/dist/schema/cw-keyer.schema.js +143 -0
  51. package/dist/schema/cw-keyer.schema.js.map +1 -0
  52. package/dist/schema/desktop-https.schema.js +1 -1
  53. package/dist/schema/desktop-https.schema.js.map +1 -1
  54. package/dist/schema/device-ui.schema.d.ts +1782 -0
  55. package/dist/schema/device-ui.schema.d.ts.map +1 -0
  56. package/dist/schema/device-ui.schema.js +156 -0
  57. package/dist/schema/device-ui.schema.js.map +1 -0
  58. package/dist/schema/logbook.schema.d.ts +2 -2
  59. package/dist/schema/mode.schema.d.ts +1 -0
  60. package/dist/schema/mode.schema.d.ts.map +1 -1
  61. package/dist/schema/mode.schema.js +8 -0
  62. package/dist/schema/mode.schema.js.map +1 -1
  63. package/dist/schema/operator.schema.d.ts +10 -10
  64. package/dist/schema/operator.schema.d.ts.map +1 -1
  65. package/dist/schema/operator.schema.js +6 -1
  66. package/dist/schema/operator.schema.js.map +1 -1
  67. package/dist/schema/plugin.schema.d.ts +653 -70
  68. package/dist/schema/plugin.schema.d.ts.map +1 -1
  69. package/dist/schema/plugin.schema.js +22 -2
  70. package/dist/schema/plugin.schema.js.map +1 -1
  71. package/dist/schema/process-monitor.schema.d.ts +3102 -42
  72. package/dist/schema/process-monitor.schema.d.ts.map +1 -1
  73. package/dist/schema/process-monitor.schema.js +54 -0
  74. package/dist/schema/process-monitor.schema.js.map +1 -1
  75. package/dist/schema/radio-capability.schema.d.ts +7 -7
  76. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  77. package/dist/schema/radio-capability.schema.js +41 -0
  78. package/dist/schema/radio-capability.schema.js.map +1 -1
  79. package/dist/schema/radio-power.schema.d.ts +6 -0
  80. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  81. package/dist/schema/radio-profile.schema.d.ts +86 -0
  82. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  83. package/dist/schema/radio.schema.d.ts +166 -9
  84. package/dist/schema/radio.schema.d.ts.map +1 -1
  85. package/dist/schema/radio.schema.js +82 -0
  86. package/dist/schema/radio.schema.js.map +1 -1
  87. package/dist/schema/realtime.schema.d.ts +6 -6
  88. package/dist/schema/slot-info.schema.d.ts +136 -0
  89. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  90. package/dist/schema/slot-info.schema.js +27 -2
  91. package/dist/schema/slot-info.schema.js.map +1 -1
  92. package/dist/schema/spectrum.schema.d.ts +24 -24
  93. package/dist/schema/voice-keyer.schema.d.ts +2 -2
  94. package/dist/schema/voice.schema.d.ts +1 -1
  95. package/dist/schema/voice.schema.d.ts.map +1 -1
  96. package/dist/schema/voice.schema.js +1 -1
  97. package/dist/schema/voice.schema.js.map +1 -1
  98. package/dist/schema/websocket.schema.d.ts +6066 -639
  99. package/dist/schema/websocket.schema.d.ts.map +1 -1
  100. package/dist/schema/websocket.schema.js +106 -4
  101. package/dist/schema/websocket.schema.js.map +1 -1
  102. package/dist/utils/__tests__/cwTiming.test.d.ts +2 -0
  103. package/dist/utils/__tests__/cwTiming.test.d.ts.map +1 -0
  104. package/dist/utils/__tests__/cwTiming.test.js +74 -0
  105. package/dist/utils/__tests__/cwTiming.test.js.map +1 -0
  106. package/dist/utils/callsign.d.ts +2 -0
  107. package/dist/utils/callsign.d.ts.map +1 -0
  108. package/dist/utils/callsign.js +8 -0
  109. package/dist/utils/callsign.js.map +1 -0
  110. package/dist/utils/cwTiming.d.ts +36 -0
  111. package/dist/utils/cwTiming.d.ts.map +1 -0
  112. package/dist/utils/cwTiming.js +155 -0
  113. package/dist/utils/cwTiming.js.map +1 -0
  114. package/package.json +1 -1
  115. package/src/index.ts +9 -0
  116. package/src/schema/__tests__/ability.test.ts +40 -0
  117. package/src/schema/__tests__/audio.schema.test.ts +4 -0
  118. package/src/schema/__tests__/cw-decoder.schema.test.ts +127 -0
  119. package/src/schema/__tests__/device-ui.schema.test.ts +122 -0
  120. package/src/schema/__tests__/operator.schema.test.ts +29 -0
  121. package/src/schema/__tests__/process-monitor.schema.test.ts +245 -0
  122. package/src/schema/__tests__/radio.schema.test.ts +160 -0
  123. package/src/schema/ability.ts +13 -0
  124. package/src/schema/api-response.schema.ts +22 -0
  125. package/src/schema/audio.schema.ts +3 -0
  126. package/src/schema/cw-decoder.schema.ts +234 -0
  127. package/src/schema/cw-keyer.schema.ts +177 -0
  128. package/src/schema/desktop-https.schema.ts +1 -1
  129. package/src/schema/device-ui.schema.ts +182 -0
  130. package/src/schema/mode.schema.ts +8 -0
  131. package/src/schema/operator.schema.ts +7 -1
  132. package/src/schema/plugin.schema.ts +28 -2
  133. package/src/schema/process-monitor.schema.ts +69 -0
  134. package/src/schema/radio-capability.schema.ts +41 -0
  135. package/src/schema/radio.schema.ts +97 -0
  136. package/src/schema/slot-info.schema.ts +33 -2
  137. package/src/schema/voice.schema.ts +1 -1
  138. package/src/schema/websocket.schema.ts +147 -4
  139. package/src/utils/__tests__/cwTiming.test.ts +91 -0
  140. package/src/utils/callsign.ts +9 -0
  141. package/src/utils/cwTiming.ts +203 -0
  142. package/test/decode-request-schema.test.ts +48 -0
@@ -0,0 +1,234 @@
1
+ import { z } from 'zod';
2
+
3
+ export const CWDecoderBackendSchema = z.enum(['deepcw-onnx']);
4
+ export type CWDecoderBackend = z.infer<typeof CWDecoderBackendSchema>;
5
+
6
+ export const CWDecoderRuntimeBackendSchema = z.enum([
7
+ 'cpu',
8
+ 'cuda',
9
+ 'coreml',
10
+ 'directml',
11
+ 'wasm',
12
+ 'webgpu',
13
+ ]);
14
+ export type CWDecoderRuntimeBackend = z.infer<typeof CWDecoderRuntimeBackendSchema>;
15
+
16
+ export const CWDecoderModelSizeSchema = z.enum(['tiny', 'small']);
17
+ export type CWDecoderModelSize = z.infer<typeof CWDecoderModelSizeSchema>;
18
+
19
+ export const CWDecoderModeSchema = z.enum(['streaming']);
20
+ export type CWDecoderMode = z.infer<typeof CWDecoderModeSchema>;
21
+
22
+ export const CWDecoderConfigSchema = z.object({
23
+ enabled: z.boolean().default(false),
24
+ backend: CWDecoderBackendSchema.default('deepcw-onnx'),
25
+ runtimeBackend: CWDecoderRuntimeBackendSchema.default('cpu'),
26
+ modelSize: CWDecoderModelSizeSchema.default('tiny'),
27
+ language: z.string().min(1).default('en'),
28
+ mode: CWDecoderModeSchema.default('streaming'),
29
+ targetFreqHz: z.number().int().positive().default(800),
30
+ filterWidthHz: z.number().int().positive().default(800),
31
+ windowSeconds: z.number().positive().default(12),
32
+ decodeIntervalMs: z.number().int().positive().default(1000),
33
+ muteWhileTransmitting: z.boolean().default(true),
34
+ workerCount: z.number().int().positive().max(4).default(1),
35
+ minCommitChars: z.number().int().positive().default(1),
36
+ commitStability: z.number().int().positive().default(2),
37
+ maxPendingAgeMs: z.number().int().positive().default(4000),
38
+ });
39
+ export type CWDecoderConfig = z.infer<typeof CWDecoderConfigSchema>;
40
+
41
+ export const CWDecoderTuningUpdateSchema = z.object({
42
+ targetFreqHz: z.number().int().min(100).max(1500).optional(),
43
+ filterWidthHz: z.number().int().min(100).max(800).optional(),
44
+ }).refine(value => value.targetFreqHz !== undefined || value.filterWidthHz !== undefined, {
45
+ message: 'At least one CW decoder tuning field is required',
46
+ });
47
+ export type CWDecoderTuningUpdate = z.infer<typeof CWDecoderTuningUpdateSchema>;
48
+
49
+ export const CWDecoderBackendDescriptorSchema = z.object({
50
+ id: CWDecoderBackendSchema,
51
+ name: z.string(),
52
+ available: z.boolean(),
53
+ runtimeBackends: z.array(CWDecoderRuntimeBackendSchema).default(['cpu']),
54
+ modelSizes: z.array(CWDecoderModelSizeSchema).default(['tiny']),
55
+ languages: z.array(z.string()).default(['en']),
56
+ modes: z.array(CWDecoderModeSchema).default(['streaming']),
57
+ version: z.string().optional(),
58
+ label: z.string().optional(),
59
+ model: z.string().optional(),
60
+ runtime: z.string().optional(),
61
+ attributionName: z.string().optional(),
62
+ sourceUrl: z.string().url().optional(),
63
+ license: z.string().optional(),
64
+ error: z.string().nullable().optional(),
65
+ reason: z.string().optional(),
66
+ });
67
+ export type CWDecoderBackendDescriptor = z.infer<typeof CWDecoderBackendDescriptorSchema>;
68
+
69
+ export const CWDecoderStatusStateSchema = z.enum([
70
+ 'disabled',
71
+ 'starting',
72
+ 'listening',
73
+ 'decoding',
74
+ 'muted',
75
+ 'error',
76
+ ]);
77
+ export type CWDecoderStatusState = z.infer<typeof CWDecoderStatusStateSchema>;
78
+
79
+ export const CWDecoderStatusSchema = z.object({
80
+ enabled: z.boolean(),
81
+ state: CWDecoderStatusStateSchema,
82
+ config: CWDecoderConfigSchema,
83
+ backend: CWDecoderBackendDescriptorSchema.optional(),
84
+ muted: z.boolean().default(false),
85
+ active: z.boolean().default(false),
86
+ lastDecodeAt: z.number().optional(),
87
+ lastError: z.string().nullable().optional(),
88
+ running: z.boolean().optional(),
89
+ backendId: CWDecoderBackendSchema.optional(),
90
+ pendingText: z.string().optional(),
91
+ committedText: z.string().optional(),
92
+ queuedSamples: z.number().int().nonnegative().optional(),
93
+ updatedAt: z.number(),
94
+ });
95
+ export type CWDecoderStatus = z.infer<typeof CWDecoderStatusSchema>;
96
+
97
+ export const CWDecoderWordSpaceSpanSchema = z.object({
98
+ startFrame: z.number().int().nonnegative(),
99
+ endFrame: z.number().int().nonnegative(),
100
+ });
101
+ export type CWDecoderWordSpaceSpan = z.infer<typeof CWDecoderWordSpaceSpanSchema>;
102
+
103
+ export const CWDecoderCharacterSpanSchema = z.object({
104
+ char: z.string(),
105
+ startFrame: z.number().int().nonnegative(),
106
+ endFrame: z.number().int().nonnegative(),
107
+ });
108
+ export type CWDecoderCharacterSpan = z.infer<typeof CWDecoderCharacterSpanSchema>;
109
+
110
+ export const CWDecoderTranscriptSegmentSchema = z.object({
111
+ id: z.string(),
112
+ sessionId: z.string(),
113
+ sequence: z.number().int().nonnegative(),
114
+ text: z.string(),
115
+ plainText: z.string().optional(),
116
+ finalized: z.literal(true).default(true),
117
+ prependSpace: z.boolean().default(true),
118
+ confidence: z.number().min(0).max(1).optional(),
119
+ targetFreqHz: z.number().positive().optional(),
120
+ filterWidthHz: z.number().positive().optional(),
121
+ characterSpans: z.array(CWDecoderCharacterSpanSchema).optional(),
122
+ wordSpaceSpans: z.array(CWDecoderWordSpaceSpanSchema).optional(),
123
+ startedAt: z.number().optional(),
124
+ endedAt: z.number().nullable().optional(),
125
+ updatedAt: z.number(),
126
+ wpm: z.number().positive().optional(),
127
+ });
128
+ export type CWDecoderTranscriptSegment = z.infer<typeof CWDecoderTranscriptSegmentSchema>;
129
+
130
+ export const CWDecoderPendingSegmentSchema = z.object({
131
+ sessionId: z.string(),
132
+ version: z.number().int().nonnegative(),
133
+ text: z.string(),
134
+ plainText: z.string().optional(),
135
+ finalized: z.literal(false).default(false),
136
+ confidence: z.number().min(0).max(1).optional(),
137
+ targetFreqHz: z.number().positive().optional(),
138
+ filterWidthHz: z.number().positive().optional(),
139
+ characterSpans: z.array(CWDecoderCharacterSpanSchema).optional(),
140
+ wordSpaceSpans: z.array(CWDecoderWordSpaceSpanSchema).optional(),
141
+ updatedAt: z.number(),
142
+ });
143
+ export type CWDecoderPendingSegment = z.infer<typeof CWDecoderPendingSegmentSchema>;
144
+
145
+ export const CWDecoderStatusEventSchema = z.object({
146
+ kind: z.literal('status'),
147
+ status: CWDecoderStatusSchema,
148
+ });
149
+
150
+ export const CWDecoderTranscriptEventSchema = z.object({
151
+ kind: z.literal('transcript'),
152
+ segment: CWDecoderTranscriptSegmentSchema,
153
+ });
154
+
155
+ export const CWDecoderTranscriptResetEventSchema = z.object({
156
+ kind: z.literal('transcript_reset'),
157
+ sessionId: z.string(),
158
+ timestamp: z.number(),
159
+ });
160
+
161
+ export const CWDecoderTranscriptPendingEventSchema = z.object({
162
+ kind: z.literal('transcript_pending'),
163
+ pending: CWDecoderPendingSegmentSchema.nullable(),
164
+ timestamp: z.number(),
165
+ });
166
+
167
+ export const CWDecoderTranscriptCommitEventSchema = z.object({
168
+ kind: z.literal('transcript_commit'),
169
+ segment: CWDecoderTranscriptSegmentSchema,
170
+ timestamp: z.number(),
171
+ });
172
+
173
+ export const CWDecoderPendingEventSchema = z.object({
174
+ kind: z.literal('pending'),
175
+ text: z.string(),
176
+ confidence: z.number().min(0).max(1).optional(),
177
+ timestamp: z.number(),
178
+ });
179
+
180
+ export const CWDecoderCommitEventSchema = z.object({
181
+ kind: z.literal('commit'),
182
+ segment: CWDecoderTranscriptSegmentSchema,
183
+ text: z.string().optional(),
184
+ confidence: z.number().min(0).max(1).optional(),
185
+ timestamp: z.number().optional(),
186
+ });
187
+
188
+ export const CWDecoderPartialEventSchema = z.object({
189
+ kind: z.literal('partial'),
190
+ text: z.string(),
191
+ confidence: z.number().min(0).max(1).optional(),
192
+ timestamp: z.number(),
193
+ });
194
+
195
+ export const CWDecoderErrorEventSchema = z.object({
196
+ kind: z.literal('error'),
197
+ message: z.string(),
198
+ code: z.string().optional(),
199
+ recoverable: z.boolean().optional(),
200
+ timestamp: z.number(),
201
+ });
202
+
203
+ export const CWDecoderModelLoadEventSchema = z.object({
204
+ kind: z.literal('model_load'),
205
+ backend: CWDecoderBackendSchema,
206
+ modelSize: CWDecoderModelSizeSchema.optional(),
207
+ language: z.string().optional(),
208
+ progress: z.number().min(0).max(1).optional(),
209
+ message: z.string().optional(),
210
+ timestamp: z.number(),
211
+ });
212
+
213
+ export const CWDecoderAudioBufferEventSchema = z.object({
214
+ kind: z.literal('audio_buffer'),
215
+ queuedSamples: z.number().int().nonnegative(),
216
+ sampleRate: z.number().int().positive().optional(),
217
+ durationMs: z.number().nonnegative().optional(),
218
+ timestamp: z.number(),
219
+ });
220
+
221
+ export const CWDecoderEventSchema = z.discriminatedUnion('kind', [
222
+ CWDecoderStatusEventSchema,
223
+ CWDecoderTranscriptEventSchema,
224
+ CWDecoderTranscriptResetEventSchema,
225
+ CWDecoderTranscriptPendingEventSchema,
226
+ CWDecoderTranscriptCommitEventSchema,
227
+ CWDecoderPendingEventSchema,
228
+ CWDecoderCommitEventSchema,
229
+ CWDecoderPartialEventSchema,
230
+ CWDecoderErrorEventSchema,
231
+ CWDecoderModelLoadEventSchema,
232
+ CWDecoderAudioBufferEventSchema,
233
+ ]);
234
+ export type CWDecoderEvent = z.infer<typeof CWDecoderEventSchema>;
@@ -0,0 +1,177 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * CW 键控器配置
5
+ */
6
+ export const CWKeyerBackendSchema = z.enum(['cat', 'serial']);
7
+ export type CWKeyerBackend = z.infer<typeof CWKeyerBackendSchema>;
8
+
9
+ export const CWKeyerConfigSchema = z.object({
10
+ /** CW 发报后端:cat=Hamlib 整报文,serial=DTR/RTS 时序键控 */
11
+ backend: CWKeyerBackendSchema.default('cat'),
12
+ /** CW 键控串口路径(用于 DTR/RTS 引脚控制) */
13
+ keyPort: z.string(),
14
+ /** CW 键控引脚类型 */
15
+ keyMethod: z.enum(['dtr', 'rts']),
16
+ /** 莫尔斯码速度 (WPM, 5-60) */
17
+ wpm: z.number().int().min(5).max(60).default(20),
18
+ });
19
+
20
+ export type CWKeyerConfig = z.infer<typeof CWKeyerConfigSchema>;
21
+
22
+ /**
23
+ * CW 键控器状态
24
+ */
25
+ export const CWKeyerStatusSchema = z.object({
26
+ /** 是否有活动键控 */
27
+ active: z.boolean(),
28
+ /** 当前状态模式 */
29
+ mode: z.enum(['idle', 'keying', 'playing', 'repeat-waiting', 'error']),
30
+ /** 发起键控的客户端 ID */
31
+ startedBy: z.string().nullable(),
32
+ /** 发起键控的客户端显示名 */
33
+ startedByLabel: z.string().nullable(),
34
+ /** 当前播放的报文 ID */
35
+ messageId: z.string().nullable(),
36
+ /** 下次执行时间戳 (repeat-waiting 时有效) */
37
+ nextRunAt: z.number().nullable(),
38
+ /** 错误信息 */
39
+ error: z.string().nullable(),
40
+ /** 当前 CW 发报后端 */
41
+ backend: CWKeyerBackendSchema.optional(),
42
+ /** 当前后端是否具备发报条件 */
43
+ backendAvailable: z.boolean().optional(),
44
+ /** 当前后端不可用原因 */
45
+ backendError: z.string().nullable().optional(),
46
+ /** 当前正在发送的文本(只读状态字段) */
47
+ currentText: z.string().nullable().optional(),
48
+ /** 最近一次发送的文本(只读状态字段) */
49
+ lastText: z.string().nullable().optional(),
50
+ });
51
+
52
+ export type CWKeyerStatus = z.infer<typeof CWKeyerStatusSchema>;
53
+
54
+ /**
55
+ * CW 报文槽位
56
+ */
57
+ export const CWMessageSlotSchema = z.object({
58
+ /** 槽位唯一 ID */
59
+ id: z.string(),
60
+ /** 显示序号 (1-based) */
61
+ index: z.number().int().min(1),
62
+ /** 报文标签 */
63
+ label: z.string(),
64
+ /** 报文文本内容 */
65
+ text: z.string(),
66
+ /** 是否启用循环播放 */
67
+ repeatEnabled: z.boolean(),
68
+ /** 循环播放间隔(秒) */
69
+ repeatIntervalSec: z.number().int().min(1).max(300),
70
+ });
71
+
72
+ export type CWMessageSlot = z.infer<typeof CWMessageSlotSchema>;
73
+
74
+ /**
75
+ * CW 报文面板
76
+ */
77
+ export const CWMessagePanelSchema = z.object({
78
+ /** 呼号 */
79
+ callsign: z.string(),
80
+ /** 当前槽位数 */
81
+ slotCount: z.number().int().min(3).max(12),
82
+ /** 最大槽位数 */
83
+ maxSlotCount: z.number().int().min(12),
84
+ /** 槽位列表 */
85
+ slots: z.array(CWMessageSlotSchema),
86
+ });
87
+
88
+ export type CWMessagePanel = z.infer<typeof CWMessagePanelSchema>;
89
+
90
+ /**
91
+ * CW 手键动作
92
+ */
93
+ export const CWKeyActionSchema = z.object({
94
+ /** 键控动作 */
95
+ action: z.enum(['key-down', 'key-up']),
96
+ });
97
+
98
+ export type CWKeyAction = z.infer<typeof CWKeyActionSchema>;
99
+
100
+ /**
101
+ * CW 报文占位符值(由前端根据当前 UI 上下文提供)
102
+ */
103
+ export const CWPlaceholderValuesSchema = z.object({
104
+ /** 当前操作员呼号,用于 {MYCALL} */
105
+ myCall: z.string().optional(),
106
+ /** 当前通联对象呼号,用于 {HISCALL} */
107
+ hisCall: z.string().optional(),
108
+ /** 发送的 RST 信号报告,用于 {TRST} */
109
+ trst: z.string().optional(),
110
+ /** 接收的 RST 信号报告,用于 {RRST} */
111
+ rrst: z.string().optional(),
112
+ });
113
+
114
+ export type CWPlaceholderValues = z.infer<typeof CWPlaceholderValuesSchema>;
115
+
116
+ /**
117
+ * CW 文字输入
118
+ */
119
+ export const CWTextInputSchema = z.object({
120
+ /** 要发送的文字 */
121
+ text: z.string().min(1).max(500),
122
+ /** 操作员呼号,用于占位符替换(如 {MYCALL}) */
123
+ callsign: z.string().optional(),
124
+ /** 前端解析出的占位符值 */
125
+ placeholderValues: CWPlaceholderValuesSchema.optional(),
126
+ });
127
+
128
+ export type CWTextInput = z.infer<typeof CWTextInputSchema>;
129
+
130
+ /**
131
+ * CW 播放预设报文请求
132
+ */
133
+ export const CWPlayMessageSchema = z.object({
134
+ /** 呼号 */
135
+ callsign: z.string(),
136
+ /** 报文槽位 ID */
137
+ slotId: z.string(),
138
+ /** 是否循环播放 */
139
+ repeat: z.boolean().optional().default(false),
140
+ /** 是否立即发送;false 时仅进入循环等待 */
141
+ startImmediately: z.boolean().optional(),
142
+ /** 前端解析出的占位符值 */
143
+ placeholderValues: CWPlaceholderValuesSchema.optional(),
144
+ });
145
+
146
+ export type CWPlayMessage = z.infer<typeof CWPlayMessageSchema>;
147
+
148
+ /**
149
+ * CW 报文槽位更新
150
+ */
151
+ export const CWMessageSlotUpdateSchema = z.object({
152
+ label: z.string().max(32).optional(),
153
+ text: z.string().max(500).optional(),
154
+ repeatEnabled: z.boolean().optional(),
155
+ repeatIntervalSec: z.number().int().min(1).max(300).optional(),
156
+ });
157
+
158
+ export type CWMessageSlotUpdate = z.infer<typeof CWMessageSlotUpdateSchema>;
159
+
160
+ /**
161
+ * CW 报文面板更新
162
+ */
163
+ export const CWMessagePanelUpdateSchema = z.object({
164
+ slotCount: z.number().int().min(3).max(12),
165
+ });
166
+
167
+ export type CWMessagePanelUpdate = z.infer<typeof CWMessagePanelUpdateSchema>;
168
+
169
+ /**
170
+ * CW 报文槽位交换请求
171
+ */
172
+ export const CWMessageSlotSwapSchema = z.object({
173
+ slotIdA: z.string(),
174
+ slotIdB: z.string(),
175
+ });
176
+
177
+ export type CWMessageSlotSwap = z.infer<typeof CWMessageSlotSwapSchema>;
@@ -23,7 +23,7 @@ export const DesktopHttpsCertificateMetaSchema = z.object({
23
23
  export type DesktopHttpsCertificateMeta = z.infer<typeof DesktopHttpsCertificateMetaSchema>;
24
24
 
25
25
  export const DesktopHttpsSettingsSchema = z.object({
26
- enabled: z.boolean().default(false),
26
+ enabled: z.boolean().default(true),
27
27
  mode: DesktopHttpsModeSchema.default('self-signed'),
28
28
  httpsPort: z.number().int().min(1).max(65535).default(8443),
29
29
  redirectExternalHttp: z.boolean().default(true),
@@ -0,0 +1,182 @@
1
+ import { z } from 'zod';
2
+
3
+ export const DEVICE_UI_JWT_TYPE = 'device-ui' as const;
4
+ export const DEVICE_UI_JWT_AUDIENCE = 'tx5dr-device-ui' as const;
5
+
6
+ export const DeviceUiJwtPayloadSchema = z.object({
7
+ typ: z.literal(DEVICE_UI_JWT_TYPE),
8
+ aud: z.literal(DEVICE_UI_JWT_AUDIENCE),
9
+ deviceId: z.string().min(1),
10
+ sessionId: z.string().min(1),
11
+ iat: z.number().int().nonnegative(),
12
+ exp: z.number().int().positive(),
13
+ });
14
+
15
+ export type DeviceUiJwtPayload = z.infer<typeof DeviceUiJwtPayloadSchema>;
16
+
17
+ export const DeviceUiSessionRequestSchema = z.object({
18
+ deviceId: z.string().trim().min(1).max(128),
19
+ sessionToken: z.string().min(1),
20
+ });
21
+
22
+ export type DeviceUiSessionRequest = z.infer<typeof DeviceUiSessionRequestSchema>;
23
+
24
+ export const DeviceUiSessionResponseSchema = z.object({
25
+ jwt: z.string().min(1),
26
+ deviceId: z.string().min(1),
27
+ sessionId: z.string().min(1),
28
+ expiresAt: z.number().int().positive(),
29
+ });
30
+
31
+ export type DeviceUiSessionResponse = z.infer<typeof DeviceUiSessionResponseSchema>;
32
+
33
+ export const DeviceUiAuthSessionStateSchema = z.object({
34
+ sessionId: z.string().min(1),
35
+ deviceId: z.string().min(1),
36
+ createdAt: z.number().int().nonnegative(),
37
+ expiresAt: z.number().int().positive(),
38
+ lastVerifiedAt: z.number().int().nonnegative().optional(),
39
+ revoked: z.boolean().optional(),
40
+ });
41
+
42
+ export type DeviceUiAuthSessionState = z.infer<typeof DeviceUiAuthSessionStateSchema>;
43
+
44
+ export const DeviceUiAuthStateSchema = z.object({
45
+ jwtSecret: z.string().min(32),
46
+ sessions: z.array(DeviceUiAuthSessionStateSchema).default([]),
47
+ updatedAt: z.number().int().nonnegative().optional(),
48
+ });
49
+
50
+ export type DeviceUiAuthState = z.infer<typeof DeviceUiAuthStateSchema>;
51
+
52
+ const NullableStringSchema = z.string().nullable();
53
+ const NullableNumberSchema = z.number().nullable();
54
+
55
+ export const DeviceUiFrameSnapshotSchema = z.object({
56
+ slotId: NullableStringSchema.optional(),
57
+ slotStartMs: NullableNumberSchema.optional(),
58
+ snr: NullableNumberSchema,
59
+ freq: NullableNumberSchema,
60
+ dt: NullableNumberSchema,
61
+ message: z.string(),
62
+ operatorId: NullableStringSchema,
63
+ country: NullableStringSchema.optional(),
64
+ countryZh: NullableStringSchema.optional(),
65
+ countryEn: NullableStringSchema.optional(),
66
+ countryCode: NullableStringSchema.optional(),
67
+ });
68
+
69
+ export const DeviceUiCurrentTxSnapshotSchema = z.object({
70
+ active: z.boolean(),
71
+ operatorIds: z.array(z.string()),
72
+ messages: z.array(z.string()),
73
+ lastMessage: NullableStringSchema,
74
+ slotStartMs: NullableNumberSchema,
75
+ });
76
+
77
+ export const DeviceUiOperatorSnapshotSchema = z.object({
78
+ id: z.string(),
79
+ callsign: z.string(),
80
+ active: z.boolean(),
81
+ transmitting: z.boolean(),
82
+ ptt: z.boolean(),
83
+ });
84
+
85
+ export const DeviceUiCwSnapshotSchema = z.object({
86
+ decoder: z.object({
87
+ enabled: z.boolean(),
88
+ active: z.boolean(),
89
+ state: z.string(),
90
+ muted: z.boolean(),
91
+ pendingText: z.string(),
92
+ committedText: z.string(),
93
+ lastDecodeAt: NullableNumberSchema,
94
+ updatedAt: z.number().int().nonnegative(),
95
+ }),
96
+ keyer: z.object({
97
+ active: z.boolean(),
98
+ mode: NullableStringSchema,
99
+ messageId: NullableStringSchema,
100
+ currentText: NullableStringSchema,
101
+ lastText: NullableStringSchema,
102
+ }),
103
+ currentTx: z.object({
104
+ active: z.boolean(),
105
+ messages: z.array(z.string()),
106
+ lastMessage: NullableStringSchema,
107
+ }),
108
+ });
109
+
110
+ export const DeviceUiBootstrapSnapshotSchema = z.object({
111
+ server: z.object({
112
+ status: z.literal('ok'),
113
+ version: z.string(),
114
+ webPort: NullableNumberSchema,
115
+ }),
116
+ station: z.object({
117
+ callsign: NullableStringSchema,
118
+ callsigns: z.array(z.string()).default([]),
119
+ }),
120
+ operators: z.array(DeviceUiOperatorSnapshotSchema).default([]),
121
+ engine: z.object({
122
+ running: z.boolean(),
123
+ mode: NullableStringSchema,
124
+ currentMode: z.object({
125
+ name: z.string(),
126
+ slotMs: z.number().nonnegative().optional(),
127
+ }).nullable(),
128
+ state: NullableStringSchema,
129
+ }),
130
+ radio: z.object({
131
+ connected: z.boolean(),
132
+ frequency: NullableNumberSchema,
133
+ radioMode: NullableStringSchema,
134
+ ptt: z.boolean(),
135
+ tx: z.boolean(),
136
+ }),
137
+ ft8: z.object({
138
+ slot: z.object({
139
+ id: z.string(),
140
+ startMs: z.number(),
141
+ phaseMs: z.number(),
142
+ driftMs: z.number().optional(),
143
+ cycleNumber: z.number(),
144
+ utcSeconds: z.number(),
145
+ mode: z.string(),
146
+ }).nullable(),
147
+ utc: NullableNumberSchema,
148
+ cycle: NullableNumberSchema,
149
+ periodMs: NullableNumberSchema,
150
+ recentDecodeRawMessages: z.array(z.string()),
151
+ lastDecodeRawMessage: NullableStringSchema,
152
+ recentFramesSlotId: NullableStringSchema,
153
+ recentFramesSlotStartMs: NullableNumberSchema,
154
+ recentFrames: z.array(DeviceUiFrameSnapshotSchema),
155
+ currentTx: DeviceUiCurrentTxSnapshotSchema,
156
+ }),
157
+ voice: z.object({
158
+ active: z.boolean(),
159
+ radioMode: NullableStringSchema,
160
+ pttLocked: z.boolean(),
161
+ pttLockedByLabel: NullableStringSchema,
162
+ keyerActive: z.boolean(),
163
+ keyerMode: NullableStringSchema,
164
+ keyerSlotId: NullableStringSchema,
165
+ }),
166
+ cw: DeviceUiCwSnapshotSchema,
167
+ access: z.object({
168
+ localUrl: NullableStringSchema,
169
+ localUrls: z.array(z.string()),
170
+ }),
171
+ updatedAt: z.number().int().nonnegative(),
172
+ });
173
+
174
+ export type DeviceUiBootstrapSnapshot = z.infer<typeof DeviceUiBootstrapSnapshotSchema>;
175
+
176
+ export const DeviceUiWsEventSchema = z.object({
177
+ type: z.literal('snapshot'),
178
+ payload: DeviceUiBootstrapSnapshotSchema,
179
+ timestamp: z.string(),
180
+ });
181
+
182
+ export type DeviceUiWsEvent = z.infer<typeof DeviceUiWsEventSchema>;
@@ -63,6 +63,14 @@ export const MODES = {
63
63
  transmitTiming: 0,
64
64
  encodeAdvance: 0,
65
65
  } as ModeDescriptor,
66
+ CW: {
67
+ name: 'CW',
68
+ slotMs: 0, // CW 模式无时隙概念
69
+ toleranceMs: 0,
70
+ windowTiming: [], // 无解码窗口
71
+ transmitTiming: 0,
72
+ encodeAdvance: 0,
73
+ } as ModeDescriptor,
66
74
  } as const;
67
75
 
68
76
  /**
@@ -1,11 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import { ModeDescriptorSchema } from './mode.schema.js';
3
+ import { sanitizeCallsignInput } from '../utils/callsign.js';
4
+
5
+ const OperatorCallsignSchema = z
6
+ .string()
7
+ .transform(sanitizeCallsignInput)
8
+ .pipe(z.string().min(1, '呼号不能为空').max(10, '呼号不能超过10个字符'));
3
9
 
4
10
  // 操作员配置 Schema (重命名以避免冲突)
5
11
  export const RadioOperatorConfigSchema = z.object({
6
12
  id: z.string().min(1, '操作员ID不能为空'),
7
13
  createdByTokenId: z.string().optional(), // 创建者 Token ID(仅用于审计,不参与权限判断)
8
- myCallsign: z.string().min(1, '呼号不能为空').max(10, '呼号不能超过10个字符'),
14
+ myCallsign: OperatorCallsignSchema,
9
15
  myGrid: z.string().min(4, '网格坐标至少4位').max(8, '网格坐标不能超过8位').optional(),
10
16
  frequency: z.number().min(0).max(1000000000, '频率必须在0-1GHz之间').default(1000), // 音频偏移频率(Hz),默认1000Hz,创建时自动分配不重复值
11
17
  transmitCycles: z.array(z.number().min(0).max(1)).default([0]), // 0=偶数周期,1=奇数周期
@@ -73,7 +73,7 @@ export type PluginPanelComponent = z.infer<typeof PluginPanelComponentSchema>;
73
73
  * These values control both validation expectations and default frontend
74
74
  * rendering in plugin settings UIs.
75
75
  */
76
- export const PluginSettingTypeSchema = z.enum(['boolean', 'number', 'string', 'string[]', 'object[]', 'info']);
76
+ export const PluginSettingTypeSchema = z.enum(['boolean', 'number', 'string', 'string[]', 'object[]', 'keyedStringArrays', 'info']);
77
77
 
78
78
  /**
79
79
  * Supported generated-form field types for plugin settings.
@@ -103,6 +103,26 @@ export const PluginObjectArrayFieldSchema = z.object({
103
103
  });
104
104
  export type PluginObjectArrayField = z.infer<typeof PluginObjectArrayFieldSchema>;
105
105
 
106
+ export const PluginKeyedStringArrayKeySchema = z.object({
107
+ key: z.string(),
108
+ label: z.string(),
109
+ description: z.string().optional(),
110
+ });
111
+ export type PluginKeyedStringArrayKey = z.infer<typeof PluginKeyedStringArrayKeySchema>;
112
+
113
+ export const PluginSettingConditionSchema = z.object({
114
+ setting: z.string(),
115
+ equals: z.unknown().optional(),
116
+ notEquals: z.unknown().optional(),
117
+ });
118
+ export type PluginSettingCondition = z.infer<typeof PluginSettingConditionSchema>;
119
+
120
+ export const PluginSettingConditionalDescriptionSchema = z.object({
121
+ when: PluginSettingConditionSchema,
122
+ description: z.string(),
123
+ });
124
+ export type PluginSettingConditionalDescription = z.infer<typeof PluginSettingConditionalDescriptionSchema>;
125
+
106
126
  /**
107
127
  * Persistence and UI scope for a plugin setting.
108
128
  *
@@ -134,6 +154,12 @@ export const PluginSettingDescriptorSchema = z.object({
134
154
  options: z.array(PluginSettingOptionSchema).optional(),
135
155
  /** Field schema used by generated editors for `object[]` settings. */
136
156
  itemFields: z.array(PluginObjectArrayFieldSchema).optional(),
157
+ /** Fixed key list used by generated editors for `keyedStringArrays` settings. */
158
+ keys: z.array(PluginKeyedStringArrayKeySchema).optional(),
159
+ /** Conditionally show the field based on another setting in the same form. */
160
+ visibleWhen: PluginSettingConditionSchema.optional(),
161
+ /** Conditionally override the field description based on another setting. */
162
+ descriptionWhen: z.array(PluginSettingConditionalDescriptionSchema).optional(),
137
163
  /** Internal settings are persisted/injected but hidden from generated UIs. */
138
164
  hidden: z.boolean().optional(),
139
165
  /** 设置作用域:global(所有操作员共享)或 operator(每操作员独立),默认 global */
@@ -212,7 +238,7 @@ export type PluginCapability = z.infer<typeof PluginCapabilitySchema>;
212
238
  * - `voice-left-top`: shown above the voice frequency control card.
213
239
  * - `voice-right-top`: shown in the tabbed top area of the voice right panel.
214
240
  */
215
- export const PluginPanelSlotSchema = z.enum(['operator', 'automation', 'main-right', 'voice-left-top', 'voice-right-top']);
241
+ export const PluginPanelSlotSchema = z.enum(['operator', 'automation', 'main-right', 'voice-left-top', 'voice-right-top', 'cw-right-top']);
216
242
 
217
243
  /**
218
244
  * Rendering slot that determines where a panel appears in the UI.