@tx5dr/contracts 2.0.0 → 2.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 (138) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/assisted-queue.schema.test.js +1 -0
  6. package/dist/schema/__tests__/assisted-queue.schema.test.js.map +1 -1
  7. package/dist/schema/__tests__/meter.schema.test.d.ts +2 -0
  8. package/dist/schema/__tests__/meter.schema.test.d.ts.map +1 -0
  9. package/dist/schema/__tests__/meter.schema.test.js +27 -0
  10. package/dist/schema/__tests__/meter.schema.test.js.map +1 -0
  11. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts +2 -0
  12. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts.map +1 -0
  13. package/dist/schema/__tests__/operator-context-report.schema.test.js +70 -0
  14. package/dist/schema/__tests__/operator-context-report.schema.test.js.map +1 -0
  15. package/dist/schema/__tests__/plugin-market.schema.test.js +107 -3
  16. package/dist/schema/__tests__/plugin-market.schema.test.js.map +1 -1
  17. package/dist/schema/__tests__/qso.schema.test.d.ts +2 -0
  18. package/dist/schema/__tests__/qso.schema.test.d.ts.map +1 -0
  19. package/dist/schema/__tests__/qso.schema.test.js +70 -0
  20. package/dist/schema/__tests__/qso.schema.test.js.map +1 -0
  21. package/dist/schema/__tests__/radio-power-support.test.js +4 -1
  22. package/dist/schema/__tests__/radio-power-support.test.js.map +1 -1
  23. package/dist/schema/__tests__/radio.schema.test.js +52 -1
  24. package/dist/schema/__tests__/radio.schema.test.js.map +1 -1
  25. package/dist/schema/__tests__/remote-access.schema.test.js +1 -1
  26. package/dist/schema/__tests__/remote-access.schema.test.js.map +1 -1
  27. package/dist/schema/__tests__/spectrum.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/spectrum.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/spectrum.schema.test.js +107 -0
  30. package/dist/schema/__tests__/spectrum.schema.test.js.map +1 -0
  31. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts +2 -0
  32. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts.map +1 -0
  33. package/dist/schema/__tests__/strategy-transmission.schema.test.js +95 -0
  34. package/dist/schema/__tests__/strategy-transmission.schema.test.js.map +1 -0
  35. package/dist/schema/api-response.schema.d.ts +235 -0
  36. package/dist/schema/api-response.schema.d.ts.map +1 -1
  37. package/dist/schema/api-response.schema.js +9 -0
  38. package/dist/schema/api-response.schema.js.map +1 -1
  39. package/dist/schema/auth.schema.d.ts +2 -2
  40. package/dist/schema/cpu-profile.schema.d.ts +2 -2
  41. package/dist/schema/device-ui.schema.d.ts +10 -10
  42. package/dist/schema/image-radio.schema.d.ts +2597 -0
  43. package/dist/schema/image-radio.schema.d.ts.map +1 -0
  44. package/dist/schema/image-radio.schema.js +283 -0
  45. package/dist/schema/image-radio.schema.js.map +1 -0
  46. package/dist/schema/logbook.schema.d.ts +203 -15
  47. package/dist/schema/logbook.schema.d.ts.map +1 -1
  48. package/dist/schema/logbook.schema.js +1 -0
  49. package/dist/schema/logbook.schema.js.map +1 -1
  50. package/dist/schema/mode.schema.d.ts +2 -0
  51. package/dist/schema/mode.schema.d.ts.map +1 -1
  52. package/dist/schema/mode.schema.js +16 -0
  53. package/dist/schema/mode.schema.js.map +1 -1
  54. package/dist/schema/openwebrx.schema.d.ts +2 -2
  55. package/dist/schema/operator.schema.d.ts +2676 -0
  56. package/dist/schema/operator.schema.d.ts.map +1 -1
  57. package/dist/schema/operator.schema.js +4 -19
  58. package/dist/schema/operator.schema.js.map +1 -1
  59. package/dist/schema/plugin.schema.d.ts +1780 -475
  60. package/dist/schema/plugin.schema.d.ts.map +1 -1
  61. package/dist/schema/plugin.schema.js +108 -12
  62. package/dist/schema/plugin.schema.js.map +1 -1
  63. package/dist/schema/qso.schema.d.ts +239 -1
  64. package/dist/schema/qso.schema.d.ts.map +1 -1
  65. package/dist/schema/qso.schema.js +61 -0
  66. package/dist/schema/qso.schema.js.map +1 -1
  67. package/dist/schema/radio-capability.schema.d.ts +1 -1
  68. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  69. package/dist/schema/radio-capability.schema.js +1 -0
  70. package/dist/schema/radio-capability.schema.js.map +1 -1
  71. package/dist/schema/radio-power.schema.d.ts +15 -0
  72. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  73. package/dist/schema/radio-profile.schema.d.ts +251 -0
  74. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  75. package/dist/schema/radio-profile.schema.js +7 -0
  76. package/dist/schema/radio-profile.schema.js.map +1 -1
  77. package/dist/schema/radio.schema.d.ts +174 -3
  78. package/dist/schema/radio.schema.d.ts.map +1 -1
  79. package/dist/schema/radio.schema.js +31 -2
  80. package/dist/schema/radio.schema.js.map +1 -1
  81. package/dist/schema/rigctld.schema.d.ts +6 -6
  82. package/dist/schema/slot-info.schema.d.ts +13 -0
  83. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  84. package/dist/schema/slot-info.schema.js +2 -1
  85. package/dist/schema/slot-info.schema.js.map +1 -1
  86. package/dist/schema/spectrum-config.schema.d.ts +638 -0
  87. package/dist/schema/spectrum-config.schema.d.ts.map +1 -0
  88. package/dist/schema/spectrum-config.schema.js +129 -0
  89. package/dist/schema/spectrum-config.schema.js.map +1 -0
  90. package/dist/schema/spectrum.schema.d.ts +1313 -20
  91. package/dist/schema/spectrum.schema.d.ts.map +1 -1
  92. package/dist/schema/spectrum.schema.js +72 -0
  93. package/dist/schema/spectrum.schema.js.map +1 -1
  94. package/dist/schema/transmission.schema.d.ts +7 -0
  95. package/dist/schema/transmission.schema.d.ts.map +1 -1
  96. package/dist/schema/tx-audio-input.schema.d.ts +8 -0
  97. package/dist/schema/tx-audio-input.schema.d.ts.map +1 -0
  98. package/dist/schema/tx-audio-input.schema.js +19 -0
  99. package/dist/schema/tx-audio-input.schema.js.map +1 -0
  100. package/dist/schema/voice.schema.d.ts +1 -1
  101. package/dist/schema/voice.schema.d.ts.map +1 -1
  102. package/dist/schema/voice.schema.js +1 -1
  103. package/dist/schema/voice.schema.js.map +1 -1
  104. package/dist/schema/websocket.schema.d.ts +27480 -2525
  105. package/dist/schema/websocket.schema.d.ts.map +1 -1
  106. package/dist/schema/websocket.schema.js +298 -11
  107. package/dist/schema/websocket.schema.js.map +1 -1
  108. package/package.json +1 -1
  109. package/src/index.ts +4 -0
  110. package/src/schema/__tests__/assisted-queue.schema.test.ts +1 -0
  111. package/src/schema/__tests__/meter.schema.test.ts +28 -0
  112. package/src/schema/__tests__/operator-context-report.schema.test.ts +77 -0
  113. package/src/schema/__tests__/plugin-market.schema.test.ts +116 -2
  114. package/src/schema/__tests__/qso.schema.test.ts +91 -0
  115. package/src/schema/__tests__/radio-power-support.test.ts +4 -1
  116. package/src/schema/__tests__/radio.schema.test.ts +60 -1
  117. package/src/schema/__tests__/remote-access.schema.test.ts +1 -1
  118. package/src/schema/__tests__/spectrum.schema.test.ts +119 -0
  119. package/src/schema/__tests__/strategy-transmission.schema.test.ts +106 -0
  120. package/src/schema/api-response.schema.ts +11 -0
  121. package/src/schema/image-radio.schema.ts +342 -0
  122. package/src/schema/logbook.schema.ts +1 -0
  123. package/src/schema/mode.schema.ts +16 -0
  124. package/src/schema/operator.schema.ts +4 -19
  125. package/src/schema/plugin.schema.ts +119 -13
  126. package/src/schema/qso.schema.ts +71 -0
  127. package/src/schema/radio-capability.schema.ts +1 -0
  128. package/src/schema/radio-profile.schema.ts +7 -0
  129. package/src/schema/radio.schema.ts +35 -2
  130. package/src/schema/slot-info.schema.ts +2 -1
  131. package/src/schema/spectrum-config.schema.ts +156 -0
  132. package/src/schema/spectrum.schema.ts +89 -0
  133. package/src/schema/tx-audio-input.schema.ts +22 -0
  134. package/src/schema/voice.schema.ts +1 -1
  135. package/src/schema/websocket.schema.ts +383 -12
  136. package/test/image-radio-schema.test.ts +107 -0
  137. package/test/plugin-slot-schema.test.ts +31 -0
  138. package/CLAUDE.md +0 -41
@@ -0,0 +1,342 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ImageFamilySchema = z.enum(['sstv', 'fax']);
4
+ export type ImageFamily = z.infer<typeof ImageFamilySchema>;
5
+
6
+ export const ImagePixelFormatSchema = z.enum(['rgb8', 'gray8']);
7
+ export type ImagePixelFormat = z.infer<typeof ImagePixelFormatSchema>;
8
+
9
+ export const ImageSstvReceiveProfileSchema = z.discriminatedUnion('strategy', [
10
+ z.object({ family: z.literal('sstv'), strategy: z.literal('auto') }),
11
+ z.object({ family: z.literal('sstv'), strategy: z.literal('manual'), mode: z.string().min(1) }),
12
+ ]);
13
+ export type ImageSstvReceiveProfile = z.infer<typeof ImageSstvReceiveProfileSchema>;
14
+
15
+ export const ImageFaxReceiveProfileSchema = z.discriminatedUnion('strategy', [
16
+ z.object({ family: z.literal('fax'), strategy: z.literal('auto') }),
17
+ z.object({
18
+ family: z.literal('fax'), strategy: z.literal('manual'),
19
+ ioc: z.enum(['ioc288', 'ioc576']), lpm: z.number().int().positive(),
20
+ modulation: z.enum(['fm', 'am']), centerHz: z.number().positive(),
21
+ deviationHz: z.number().positive(),
22
+ }),
23
+ ]);
24
+ export type ImageFaxReceiveProfile = z.infer<typeof ImageFaxReceiveProfileSchema>;
25
+
26
+ export const ImageReceiveProfileSchema = z.union([
27
+ ImageSstvReceiveProfileSchema,
28
+ ImageFaxReceiveProfileSchema,
29
+ ]);
30
+ export type ImageReceiveProfile = z.infer<typeof ImageReceiveProfileSchema>;
31
+
32
+ export const ImageSstvModeInfoSchema = z.object({
33
+ mode: z.string(), name: z.string(), visCode: z.number().int().nonnegative(),
34
+ width: z.number().int().positive(), height: z.number().int().positive(),
35
+ colorLayout: z.enum(['monochrome', 'rgb', 'yuv']),
36
+ scanLayout: z.enum(['monochrome', 'martin', 'scottie', 'robot', 'pd', 'wraase', 'pasokon']),
37
+ lineSeconds: z.number().positive(), rowsPerLine: z.number().int().positive(),
38
+ status: z.enum(['canonical', 'compatibility']),
39
+ });
40
+ export type ImageSstvModeInfo = z.infer<typeof ImageSstvModeInfoSchema>;
41
+
42
+ export const ImageRadioCapabilitySchema = z.object({
43
+ available: z.boolean(),
44
+ reason: z.string().optional(),
45
+ sstv: z.object({ rx: z.literal(true), tx: z.literal(true) }),
46
+ fax: z.object({ rx: z.literal(true), tx: z.literal(false) }),
47
+ });
48
+ export type ImageRadioCapability = z.infer<typeof ImageRadioCapabilitySchema>;
49
+
50
+ export const ImageRadioServiceStateSchema = z.enum(['stopped', 'starting', 'ready', 'degraded', 'unavailable']);
51
+ export const ImageRxStateSchema = z.enum(['off', 'searching', 'acquiring', 'receiving', 'completed', 'aborted', 'error']);
52
+ export const SstvTxPhaseSchema = z.enum([
53
+ 'idle', 'preparing', 'waiting_for_lease', 'keying', 'on_air', 'draining',
54
+ 'completed', 'cancelled', 'error', 'ptt_unknown',
55
+ ]);
56
+ export const SstvStationIdModeSchema = z.enum(['fsk', 'cw', 'none']);
57
+ export type SstvStationIdMode = z.infer<typeof SstvStationIdModeSchema>;
58
+ export const SstvTxEnvelopeSelectionSchema = z.object({
59
+ enhancedPreamble: z.boolean().default(true),
60
+ stationIdMode: SstvStationIdModeSchema.default('fsk'),
61
+ });
62
+ export type SstvTxEnvelopeSelection = z.infer<typeof SstvTxEnvelopeSelectionSchema>;
63
+ export const SstvTxEnvelopeSnapshotSchema = SstvTxEnvelopeSelectionSchema.extend({
64
+ callsign: z.string().optional(),
65
+ postImageGapMs: z.literal(500),
66
+ endGuardMs: z.literal(300),
67
+ cwWpm: z.literal(20),
68
+ cwToneHz: z.literal(800),
69
+ });
70
+ export type SstvTxEnvelopeSnapshot = z.infer<typeof SstvTxEnvelopeSnapshotSchema>;
71
+ export const SstvTxPreferencesSchema = SstvTxEnvelopeSelectionSchema.extend({
72
+ operatorId: z.string().min(1),
73
+ updatedAt: z.number(),
74
+ });
75
+ export type SstvTxPreferences = z.infer<typeof SstvTxPreferencesSchema>;
76
+
77
+ export const ImageSessionSummarySchema = z.object({
78
+ sessionId: z.string(),
79
+ family: ImageFamilySchema,
80
+ generation: z.number().int().nonnegative(),
81
+ revision: z.number().int().nonnegative(),
82
+ codecMode: z.string().optional(),
83
+ width: z.number().int().positive().optional(),
84
+ height: z.number().int().positive().optional(),
85
+ receivedLines: z.number().int().nonnegative().default(0),
86
+ firstAvailableLine: z.number().int().nonnegative().default(0),
87
+ startedAt: z.number(),
88
+ });
89
+ export type ImageSessionSummary = z.infer<typeof ImageSessionSummarySchema>;
90
+
91
+ export const SstvTxStatusSchema = z.object({
92
+ phase: SstvTxPhaseSchema,
93
+ sessionId: z.string().optional(),
94
+ requestId: z.string().optional(),
95
+ operatorId: z.string().optional(),
96
+ artifactId: z.string().optional(),
97
+ historyId: z.string().optional(),
98
+ mode: z.string().optional(),
99
+ encoderStage: z.enum(['preamble', 'vis', 'raster', 'stationId', 'guard', 'finished']).optional(),
100
+ envelope: SstvTxEnvelopeSnapshotSchema.optional(),
101
+ revision: z.number().int().nonnegative().default(0),
102
+ samplesEmitted: z.number().int().nonnegative().default(0),
103
+ estimatedTotalSamples: z.number().int().nonnegative().default(0),
104
+ currentRow: z.number().int().nonnegative().optional(),
105
+ startedAt: z.number().optional(),
106
+ errorCode: z.string().optional(),
107
+ });
108
+ export type SstvTxStatus = z.infer<typeof SstvTxStatusSchema>;
109
+
110
+ export const ImageRadioStatusSchema = z.object({
111
+ serviceState: ImageRadioServiceStateSchema,
112
+ family: ImageFamilySchema.nullable(),
113
+ receiveProfile: ImageReceiveProfileSchema.nullable(),
114
+ rxState: ImageRxStateSchema,
115
+ rxCaptureActive: z.boolean().default(false),
116
+ capability: ImageRadioCapabilitySchema,
117
+ currentSession: ImageSessionSummarySchema.nullable(),
118
+ tx: SstvTxStatusSchema,
119
+ nativeQueuedSamples: z.number().int().nonnegative().default(0),
120
+ jsBacklogSamples: z.number().int().nonnegative().default(0),
121
+ discontinuities: z.number().int().nonnegative().default(0),
122
+ updatedAt: z.number(),
123
+ });
124
+ export type ImageRadioStatus = z.infer<typeof ImageRadioStatusSchema>;
125
+
126
+ export const ImageRowPatchSchema = z.object({
127
+ rowIndex: z.number().int().nonnegative(),
128
+ width: z.number().int().positive(),
129
+ rowRevision: z.number().int().nonnegative(),
130
+ completeness: z.enum(['provisional', 'final']).optional(),
131
+ dataBase64: z.string(),
132
+ });
133
+
134
+ export const ImagePaperBoundaryKindSchema = z.enum([
135
+ 'initial', 'vis', 'syncTiming', 'aptPhasing', 'protocolEnd',
136
+ 'manualMode', 'discontinuity', 'reset', 'truncated', 'protocolObserved',
137
+ 'localTxStart', 'localTxEnd',
138
+ ]);
139
+ export type ImagePaperBoundaryKind = z.infer<typeof ImagePaperBoundaryKindSchema>;
140
+ export const ImagePaperSourceSchema = z.enum(['rx', 'localTx']);
141
+ export type ImagePaperSource = z.infer<typeof ImagePaperSourceSchema>;
142
+
143
+ export const ImagePaperBoundarySchema = z.object({
144
+ boundaryId: z.string(), lineIndex: z.number().int().nonnegative(),
145
+ kind: ImagePaperBoundaryKindSchema, trusted: z.boolean(),
146
+ codecMode: z.string(), width: z.number().int().positive(),
147
+ pixelFormat: ImagePixelFormatSchema, timestamp: z.number(),
148
+ detection: z.string().optional(), nominalHeight: z.number().int().positive().optional(),
149
+ source: ImagePaperSourceSchema.optional(),
150
+ txSessionId: z.string().optional(),
151
+ txOutcome: z.enum(['completed', 'interrupted']).optional(),
152
+ });
153
+ export type ImagePaperBoundary = z.infer<typeof ImagePaperBoundarySchema>;
154
+
155
+ export const ImageFaxCalibrationPointSchema = z.object({
156
+ revision: z.number().int().nonnegative(),
157
+ referenceLine: z.number().int().nonnegative(),
158
+ phasePixels: z.number().finite(),
159
+ clockPpm: z.number().finite(),
160
+ confidence: z.number().min(0).max(1),
161
+ source: z.enum(['nominal', 'phasing', 'imageContent', 'manual']),
162
+ status: z.enum(['nominal', 'acquiring', 'locked', 'tracking', 'degraded']),
163
+ });
164
+ export type ImageFaxCalibrationPoint = z.infer<typeof ImageFaxCalibrationPointSchema>;
165
+
166
+ export const ImageFaxCalibrationSchema = z.object({
167
+ boundaryId: z.string().min(1),
168
+ revision: z.number().int().nonnegative(),
169
+ autoEnabled: z.boolean(),
170
+ autoPoints: z.array(ImageFaxCalibrationPointSchema).max(256),
171
+ manualPhasePixels: z.number().finite(),
172
+ manualClockPpm: z.number().finite(),
173
+ updatedAt: z.number(),
174
+ });
175
+ export type ImageFaxCalibration = z.infer<typeof ImageFaxCalibrationSchema>;
176
+
177
+ export const ImageRxEventSchema = z.discriminatedUnion('type', [
178
+ z.object({ type: z.literal('paperStarted'), session: ImageSessionSummarySchema, pixelFormat: ImagePixelFormatSchema }),
179
+ z.object({ type: z.literal('boundary'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), boundary: ImagePaperBoundarySchema }),
180
+ z.object({ type: z.literal('faxCalibration'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), calibration: ImageFaxCalibrationSchema }),
181
+ z.object({ type: z.literal('signalDetected'), family: ImageFamilySchema, confidence: z.number().min(0).max(1), candidates: z.array(z.string()), timestamp: z.number() }),
182
+ z.object({ type: z.literal('imageStarted'), session: ImageSessionSummarySchema, pixelFormat: ImagePixelFormatSchema, detection: z.string().optional(), confidence: z.number().min(0).max(1).optional() }),
183
+ z.object({ type: z.literal('rows'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), pixelFormat: ImagePixelFormatSchema, rows: z.array(ImageRowPatchSchema).min(1).max(8) }),
184
+ z.object({ type: z.literal('imageCompleted'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), artifactId: z.string(), previewUrl: z.string(), partial: z.boolean() }),
185
+ z.object({ type: z.literal('imageAborted'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), reason: z.string(), lastRow: z.number().int().nonnegative().optional(), temporary: z.boolean() }),
186
+ z.object({ type: z.literal('snapshotRequired'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), snapshotUrl: z.string() }),
187
+ z.object({ type: z.literal('captureSaved'), sessionId: z.string(), generation: z.number().int().nonnegative(), revision: z.number().int().nonnegative(), artifactId: z.string(), previewUrl: z.string(), saveReason: z.enum(['manual', 'protocolEnd']), complete: z.boolean(), startLine: z.number().int().nonnegative(), endLine: z.number().int().positive() }),
188
+ ]);
189
+ export type ImageRxEvent = z.infer<typeof ImageRxEventSchema>;
190
+
191
+ export const ImageArtifactSchema = z.object({
192
+ id: z.string(),
193
+ family: ImageFamilySchema,
194
+ direction: z.enum(['rx', 'tx']),
195
+ operatorId: z.string().optional(),
196
+ codecMode: z.string(),
197
+ pixelFormat: ImagePixelFormatSchema,
198
+ width: z.number().int().positive(),
199
+ height: z.number().int().positive(),
200
+ frequency: z.number().positive(),
201
+ radioMode: z.string().optional(),
202
+ complete: z.boolean(),
203
+ saveReason: z.enum(['manual', 'protocolEnd']).optional(),
204
+ captureStartedAt: z.number().optional(),
205
+ captureEndedAt: z.number().optional(),
206
+ truncated: z.boolean().default(false),
207
+ pinned: z.boolean().default(false),
208
+ qsoId: z.string().optional(),
209
+ contentHash: z.string(),
210
+ createdAt: z.number(),
211
+ imageUrl: z.string(),
212
+ faxCalibration: ImageFaxCalibrationSchema.optional(),
213
+ });
214
+ export type ImageArtifact = z.infer<typeof ImageArtifactSchema>;
215
+
216
+ const ImageHistoryRecordBaseSchema = z.object({
217
+ id: z.string(),
218
+ artifactId: z.string(),
219
+ family: ImageFamilySchema,
220
+ operatorId: z.string().optional(),
221
+ occurredAt: z.number(),
222
+ qsoId: z.string().optional(),
223
+ });
224
+
225
+ export const ImageHistoryRecordSchema = z.discriminatedUnion('direction', [
226
+ ImageHistoryRecordBaseSchema.extend({
227
+ direction: z.literal('rx'),
228
+ saveReason: z.enum(['manual', 'protocolEnd']),
229
+ complete: z.boolean(),
230
+ truncated: z.boolean().default(false),
231
+ }),
232
+ ImageHistoryRecordBaseSchema.extend({
233
+ direction: z.literal('tx'),
234
+ operatorId: z.string().min(1),
235
+ sessionId: z.string().min(1),
236
+ startedAt: z.number(),
237
+ endedAt: z.number().optional(),
238
+ outcome: z.enum(['transmitting', 'completed', 'interrupted']),
239
+ errorCode: z.string().optional(),
240
+ envelope: SstvTxEnvelopeSnapshotSchema.optional(),
241
+ sampleRate: z.number().int().positive().optional(),
242
+ estimatedTotalSamples: z.number().int().nonnegative().optional(),
243
+ }),
244
+ ]);
245
+ export type ImageHistoryRecord = z.infer<typeof ImageHistoryRecordSchema>;
246
+
247
+ export const ImageHistoryEntrySchema = z.object({
248
+ record: ImageHistoryRecordSchema,
249
+ artifact: ImageArtifactSchema,
250
+ });
251
+ export type ImageHistoryEntry = z.infer<typeof ImageHistoryEntrySchema>;
252
+
253
+ export const ImageTemplateTextLayerSchema = z.object({
254
+ id: z.string().min(1).max(64),
255
+ text: z.string().max(256),
256
+ x: z.number().min(-2).max(2),
257
+ y: z.number().min(-2).max(2),
258
+ width: z.number().positive().max(4),
259
+ height: z.number().positive().max(4),
260
+ fontSize: z.number().positive().max(1),
261
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
262
+ strokeColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
263
+ strokeWidth: z.number().min(0).max(0.5).default(0.12),
264
+ align: z.enum(['left', 'center', 'right']).default('center'),
265
+ rotation: z.number().min(-180).max(180).default(0),
266
+ });
267
+ export type ImageTemplateTextLayer = z.infer<typeof ImageTemplateTextLayerSchema>;
268
+
269
+ export const ImageTemplateSchema = z.object({
270
+ id: z.string(),
271
+ operatorId: z.string().optional(),
272
+ name: z.string().min(1).max(80),
273
+ builtIn: z.boolean().default(false),
274
+ backgroundArtifactId: z.string().optional(),
275
+ layers: z.array(ImageTemplateTextLayerSchema).max(16),
276
+ createdAt: z.number(),
277
+ updatedAt: z.number(),
278
+ });
279
+ export type ImageTemplate = z.infer<typeof ImageTemplateSchema>;
280
+
281
+ export const ImageComposerBackgroundSchema = z.object({
282
+ operatorId: z.string().min(1),
283
+ width: z.number().int().positive(),
284
+ height: z.number().int().positive(),
285
+ updatedAt: z.number(),
286
+ imageUrl: z.string(),
287
+ });
288
+ export type ImageComposerBackground = z.infer<typeof ImageComposerBackgroundSchema>;
289
+
290
+ export const ImageRxSubscriptionSchema = z.object({ enabled: z.boolean() });
291
+ export const SstvTxStartCommandSchema = z.object({
292
+ requestId: z.string().min(1).max(128),
293
+ operatorId: z.string().min(1),
294
+ artifactId: z.string().min(1),
295
+ mode: z.string().min(1),
296
+ expectedFrequency: z.number().positive(),
297
+ interruptActiveCapture: z.boolean().optional(),
298
+ envelope: SstvTxEnvelopeSelectionSchema.default({ enhancedPreamble: true, stationIdMode: 'fsk' }),
299
+ });
300
+ export type SstvTxStartCommand = z.infer<typeof SstvTxStartCommandSchema>;
301
+ export const SstvTxCancelCommandSchema = z.object({
302
+ requestId: z.string().min(1).max(128),
303
+ operatorId: z.string().min(1),
304
+ sessionId: z.string().min(1),
305
+ expectedRevision: z.number().int().nonnegative(),
306
+ });
307
+ export type SstvTxCancelCommand = z.infer<typeof SstvTxCancelCommandSchema>;
308
+ export const SstvTxCommandResultSchema = z.object({
309
+ requestId: z.string(),
310
+ accepted: z.boolean(),
311
+ sessionId: z.string().optional(),
312
+ errorCode: z.string().optional(),
313
+ });
314
+ export type SstvTxCommandResult = z.infer<typeof SstvTxCommandResultSchema>;
315
+
316
+ export const ImagePaperSaveCommandSchema = z.object({
317
+ requestId: z.string().min(1).max(128),
318
+ operatorId: z.string().min(1),
319
+ expectedRevision: z.number().int().nonnegative(),
320
+ });
321
+ export type ImagePaperSaveCommand = z.infer<typeof ImagePaperSaveCommandSchema>;
322
+
323
+ const FaxCalibrationCommandBaseSchema = z.object({
324
+ requestId: z.string().min(1).max(128),
325
+ operatorId: z.string().min(1),
326
+ sessionId: z.string().min(1),
327
+ boundaryId: z.string().min(1),
328
+ expectedRevision: z.number().int().nonnegative(),
329
+ });
330
+ export const FaxCalibrationSetCommandSchema = FaxCalibrationCommandBaseSchema.extend({
331
+ autoEnabled: z.boolean(),
332
+ phasePixels: z.number().finite(),
333
+ clockPpm: z.number().finite().min(-5000).max(5000),
334
+ });
335
+ export type FaxCalibrationSetCommand = z.infer<typeof FaxCalibrationSetCommandSchema>;
336
+ export const FaxCalibrationResetCommandSchema = FaxCalibrationCommandBaseSchema;
337
+ export type FaxCalibrationResetCommand = z.infer<typeof FaxCalibrationResetCommandSchema>;
338
+ export const FaxCalibrationCommandResultSchema = z.object({
339
+ requestId: z.string(), accepted: z.boolean(),
340
+ calibration: ImageFaxCalibrationSchema.optional(), errorCode: z.string().optional(),
341
+ });
342
+ export type FaxCalibrationCommandResult = z.infer<typeof FaxCalibrationCommandResultSchema>;
@@ -12,6 +12,7 @@ export const LOGBOOK_OPERATION_ERROR_CODES = [
12
12
  'LOGBOOK_BACKUP_UNAVAILABLE',
13
13
  'LOGBOOK_BACKUP_CHANGED',
14
14
  'LOGBOOK_MAINTENANCE',
15
+ 'LOGBOOK_REVISION_CONFLICT',
15
16
  'LOGBOOK_REVISION_MISMATCH',
16
17
  'LOGBOOK_RESTORE_PRECONDITION_FAILED',
17
18
  'LOGBOOK_PRECONDITION_REQUIRED',
@@ -69,6 +69,22 @@ export const MODES = {
69
69
  transmitTiming: 0,
70
70
  encodeAdvance: 0,
71
71
  } as ModeDescriptor,
72
+ SSTV: {
73
+ name: 'SSTV',
74
+ slotMs: 0,
75
+ toleranceMs: 0,
76
+ windowTiming: [],
77
+ transmitTiming: 0,
78
+ encodeAdvance: 0,
79
+ } as ModeDescriptor,
80
+ FAX: {
81
+ name: 'FAX',
82
+ slotMs: 0,
83
+ toleranceMs: 0,
84
+ windowTiming: [],
85
+ transmitTiming: 0,
86
+ encodeAdvance: 0,
87
+ } as ModeDescriptor,
72
88
  } as const;
73
89
 
74
90
  /**
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { ModeDescriptorSchema } from './mode.schema.js';
3
3
  import { sanitizeCallsignInput } from '../utils/callsign.js';
4
4
  import { sanitizeGridInput } from '../utils/grid.js';
5
+ import { StrategyRuntimeSnapshotSchema } from './websocket.schema.js';
5
6
 
6
7
  const OperatorCallsignSchema = z
7
8
  .string()
@@ -20,6 +21,7 @@ export const RadioOperatorConfigSchema = z.object({
20
21
  myCallsign: OperatorCallsignSchema,
21
22
  myGrid: OperatorGridSchema.optional(),
22
23
  frequency: z.number().min(0).max(1000000000, '频率必须在0-1GHz之间').default(1000), // 音频偏移频率(Hz),默认1000Hz,创建时自动分配不重复值
24
+ maxConcurrentStreams: z.number().int().min(1).max(5).optional(),
23
25
  transmitCycles: z.array(z.number().min(0).max(1)).default([0]), // 0=偶数周期,1=奇数周期
24
26
  mode: ModeDescriptorSchema.optional(),
25
27
  logBookId: z.string().optional(), // 连接的日志本ID,如果未指定则使用默认日志本
@@ -67,6 +69,7 @@ export const RadioOperatorStatusResponseSchema = z.object({
67
69
  context: z.object({
68
70
  myCall: z.string(),
69
71
  myGrid: z.string(),
72
+ targetCalls: z.array(z.string()).optional(),
70
73
  targetCall: z.string(),
71
74
  targetGrid: z.string().optional(),
72
75
  frequency: z.number().optional(),
@@ -78,25 +81,7 @@ export const RadioOperatorStatusResponseSchema = z.object({
78
81
  state: z.string(),
79
82
  availableSlots: z.array(z.string()),
80
83
  }),
81
- runtime: z.object({
82
- currentState: z.string(),
83
- slots: z.object({
84
- TX1: z.string().optional(),
85
- TX2: z.string().optional(),
86
- TX3: z.string().optional(),
87
- TX4: z.string().optional(),
88
- TX5: z.string().optional(),
89
- TX6: z.string().optional(),
90
- }).optional(),
91
- context: z.object({
92
- targetCallsign: z.string().optional(),
93
- targetGrid: z.string().optional(),
94
- reportSent: z.number().optional(),
95
- reportReceived: z.number().optional(),
96
- actualFrequency: z.number().optional(),
97
- }).optional(),
98
- availableSlots: z.array(z.string()).optional(),
99
- }).optional(),
84
+ runtime: StrategyRuntimeSnapshotSchema.optional(),
100
85
  slots: z.object({
101
86
  TX1: z.string().optional(),
102
87
  TX2: z.string().optional(),
@@ -19,6 +19,11 @@ export type PluginType = z.infer<typeof PluginTypeSchema>;
19
19
 
20
20
  export const StrategyFeaturesSchema = z.object({
21
21
  targetQueue: z.literal(1).optional(),
22
+ parallelTargetQueue: z.literal(1).optional(),
23
+ queueActivation: z.enum(['immediate', 'operator-toggle']).optional(),
24
+ manualInitiation: z.literal(1).optional(),
25
+ maxConcurrentStreams: z.number().int().min(1).max(5).optional(),
26
+ maxSimultaneousSignals: z.number().int().min(1).max(5).optional(),
22
27
  }).optional();
23
28
  export type StrategyFeatures = z.infer<typeof StrategyFeaturesSchema>;
24
29
 
@@ -52,6 +57,7 @@ export const PluginPermissionSchema = z.enum([
52
57
  'radio:power',
53
58
  'logbook:read',
54
59
  'logbook:write',
60
+ 'logbook:session',
55
61
  'logbook:sync',
56
62
  'settings:ft8',
57
63
  'settings:decode-windows',
@@ -299,6 +305,7 @@ export type PluginCapability = z.infer<typeof PluginCapabilitySchema>;
299
305
  *
300
306
  * - `operator`: shown in the expanded operator card's live-panel area (default).
301
307
  * - `automation`: shown inside the top-right automation quick-action popover.
308
+ * - `operator-action`: shown beside the built-in logbook action on an operator card.
302
309
  * - `main-right`: shown in the optional main layout plugin pane on the far right.
303
310
  * - `voice-left-top`: shown above the voice frequency control card.
304
311
  * - `voice-right-top`: shown in the tabbed top area of the voice right panel.
@@ -308,6 +315,7 @@ export type PluginCapability = z.infer<typeof PluginCapabilitySchema>;
308
315
  export const PluginPanelSlotSchema = z.enum([
309
316
  'operator',
310
317
  'automation',
318
+ 'operator-action',
311
319
  'main-right',
312
320
  'voice-left-top',
313
321
  'voice-right-top',
@@ -336,12 +344,15 @@ export const PluginPanelWidthSchema = z.enum(['half', 'full']);
336
344
  export type PluginPanelWidth = z.infer<typeof PluginPanelWidthSchema>;
337
345
 
338
346
  /**
339
- * How an iframe panel is opened when rendered as a toolbar entry.
347
+ * How an iframe panel is opened when rendered as an action entry.
348
+ *
349
+ * `page` opens the referenced custom UI in the host's standalone plugin-page
350
+ * route instead of embedding it in the radio workspace.
340
351
  */
341
- export const PluginPanelOpenModeSchema = z.enum(['popover', 'modal']);
352
+ export const PluginPanelOpenModeSchema = z.enum(['popover', 'modal', 'page']);
342
353
 
343
354
  /**
344
- * How an iframe panel is opened when rendered as a toolbar entry.
355
+ * How an iframe panel is opened when rendered as an action entry.
345
356
  */
346
357
  export type PluginPanelOpenMode = z.infer<typeof PluginPanelOpenModeSchema>;
347
358
 
@@ -382,21 +393,28 @@ export const PluginPanelDescriptorSchema = z.object({
382
393
  /** Optional controlled UI size hint for toolbar popovers/modals. */
383
394
  uiSize: PluginPanelUISizeSchema.optional(),
384
395
  }).superRefine((panel, ctx) => {
385
- if (panel.slot !== 'radio-control-toolbar') {
396
+ if (panel.slot !== 'radio-control-toolbar' && panel.slot !== 'operator-action') {
386
397
  return;
387
398
  }
388
399
  if (panel.component !== 'iframe') {
389
400
  ctx.addIssue({
390
401
  code: z.ZodIssueCode.custom,
391
402
  path: ['component'],
392
- message: 'radio-control-toolbar panels must use iframe component',
403
+ message: `${panel.slot} panels must use iframe component`,
393
404
  });
394
405
  }
395
406
  if (!panel.pageId) {
396
407
  ctx.addIssue({
397
408
  code: z.ZodIssueCode.custom,
398
409
  path: ['pageId'],
399
- message: 'radio-control-toolbar panels must declare pageId',
410
+ message: `${panel.slot} panels must declare pageId`,
411
+ });
412
+ }
413
+ if (panel.slot === 'operator-action' && panel.openMode !== 'page') {
414
+ ctx.addIssue({
415
+ code: z.ZodIssueCode.custom,
416
+ path: ['openMode'],
417
+ message: 'operator-action panels must use page openMode',
400
418
  });
401
419
  }
402
420
  });
@@ -504,6 +522,11 @@ export type PluginStorageConfig = z.infer<typeof PluginStorageConfigSchema>;
504
522
 
505
523
  // ===== 插件清单 =====
506
524
 
525
+ const SemanticVersionSchema = z.string().regex(
526
+ /^v?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/,
527
+ 'must be a semantic version',
528
+ );
529
+
507
530
  /**
508
531
  * Normalized manifest describing a plugin's static metadata and declarations.
509
532
  *
@@ -514,6 +537,7 @@ export const PluginManifestSchema = z.object({
514
537
  apiVersion: z.literal(2).optional(),
515
538
  name: z.string(),
516
539
  version: z.string(),
540
+ minPluginApiVersion: SemanticVersionSchema.optional(),
517
541
  type: PluginTypeSchema,
518
542
  strategyFeatures: StrategyFeaturesSchema,
519
543
  instanceScope: PluginInstanceScopeSchema.optional().default('operator'),
@@ -647,6 +671,8 @@ export const PluginDistributionSchema = z.enum([
647
671
  export type PluginDistribution = z.infer<typeof PluginDistributionSchema>;
648
672
 
649
673
  export const PluginRuntimeInfoSchema = z.object({
674
+ hostVersion: z.string(),
675
+ pluginApiVersion: SemanticVersionSchema,
650
676
  pluginDir: z.string(),
651
677
  pluginDataDir: z.string(),
652
678
  dataDir: z.string(),
@@ -677,7 +703,7 @@ export const PluginMarketScreenshotSchema = z.object({
677
703
  });
678
704
  export type PluginMarketScreenshot = z.infer<typeof PluginMarketScreenshotSchema>;
679
705
 
680
- export const PluginMarketCatalogEntrySchema = z.object({
706
+ const PluginMarketCatalogEntryInputSchema = z.object({
681
707
  name: z.string(),
682
708
  title: z.string(),
683
709
  description: z.string(),
@@ -685,7 +711,9 @@ export const PluginMarketCatalogEntrySchema = z.object({
685
711
  readmeSourceUrl: z.string().url().optional(),
686
712
  locales: PluginLocalesSchema.optional(),
687
713
  latestVersion: z.string(),
688
- minHostVersion: z.string(),
714
+ minPluginApiVersion: SemanticVersionSchema.optional(),
715
+ /** Catalog schema v1 compatibility alias; normalized on read. */
716
+ minHostVersion: SemanticVersionSchema.optional(),
689
717
  author: z.string().optional(),
690
718
  license: z.string().optional(),
691
719
  repository: z.string().url().optional(),
@@ -698,15 +726,93 @@ export const PluginMarketCatalogEntrySchema = z.object({
698
726
  sha256: z.string().regex(/^[a-f0-9]{64}$/i),
699
727
  size: z.number().int().nonnegative(),
700
728
  publishedAt: z.string(),
729
+ artifactManifestVersion: z.literal(1).optional(),
730
+ }).superRefine((value, context) => {
731
+ if (!value.minPluginApiVersion && !value.minHostVersion) {
732
+ context.addIssue({
733
+ code: z.ZodIssueCode.custom,
734
+ path: ['minPluginApiVersion'],
735
+ message: 'minPluginApiVersion is required',
736
+ });
737
+ }
738
+ if (value.minPluginApiVersion && value.minHostVersion
739
+ && value.minPluginApiVersion !== value.minHostVersion) {
740
+ context.addIssue({
741
+ code: z.ZodIssueCode.custom,
742
+ path: ['minPluginApiVersion'],
743
+ message: 'minPluginApiVersion conflicts with legacy minHostVersion',
744
+ });
745
+ }
746
+ });
747
+
748
+ export const PluginMarketCatalogEntrySchema = PluginMarketCatalogEntryInputSchema.transform((value) => {
749
+ const { minHostVersion: _legacyMinimum, ...entry } = value;
750
+ return {
751
+ ...entry,
752
+ minPluginApiVersion: value.minPluginApiVersion ?? value.minHostVersion!,
753
+ };
701
754
  });
702
755
  export type PluginMarketCatalogEntry = z.infer<typeof PluginMarketCatalogEntrySchema>;
703
756
 
704
- export const PluginMarketCatalogSchema = z.object({
705
- schemaVersion: z.number().int().positive(),
706
- generatedAt: z.string(),
707
- channel: PluginMarketChannelSchema,
708
- plugins: z.array(PluginMarketCatalogEntrySchema),
757
+ const PluginMarketCatalogV2EntrySchema = PluginMarketCatalogEntryInputSchema.superRefine(
758
+ (value, context) => {
759
+ if (!value.minPluginApiVersion) {
760
+ context.addIssue({
761
+ code: z.ZodIssueCode.custom,
762
+ path: ['minPluginApiVersion'],
763
+ message: 'catalog schema v2 requires minPluginApiVersion',
764
+ });
765
+ }
766
+ if (value.minHostVersion) {
767
+ context.addIssue({
768
+ code: z.ZodIssueCode.custom,
769
+ path: ['minHostVersion'],
770
+ message: 'catalog schema v2 does not support legacy minHostVersion',
771
+ });
772
+ }
773
+ if (value.artifactManifestVersion !== 1) {
774
+ context.addIssue({
775
+ code: z.ZodIssueCode.custom,
776
+ path: ['artifactManifestVersion'],
777
+ message: 'catalog schema v2 requires artifactManifestVersion 1',
778
+ });
779
+ }
780
+ },
781
+ ).transform((value) => {
782
+ const { minHostVersion: _legacyMinimum, ...entry } = value;
783
+ return {
784
+ ...entry,
785
+ minPluginApiVersion: value.minPluginApiVersion!,
786
+ };
787
+ });
788
+
789
+ export const PluginArtifactManifestSchema = z.object({
790
+ schemaVersion: z.literal(1),
791
+ name: z.string(),
792
+ version: z.string(),
793
+ minPluginApiVersion: SemanticVersionSchema,
794
+ apiVersion: z.literal(2).optional(),
795
+ type: PluginTypeSchema,
796
+ instanceScope: PluginInstanceScopeSchema.optional().default('operator'),
797
+ permissions: z.array(PluginPermissionSchema).optional().default([]),
798
+ strategyFeatures: StrategyFeaturesSchema,
709
799
  });
800
+ export type PluginArtifactManifest = z.infer<typeof PluginArtifactManifestSchema>;
801
+
802
+ export const PluginMarketCatalogSchema = z.discriminatedUnion('schemaVersion', [
803
+ z.object({
804
+ schemaVersion: z.literal(1),
805
+ generatedAt: z.string(),
806
+ channel: PluginMarketChannelSchema,
807
+ plugins: z.array(PluginMarketCatalogEntrySchema),
808
+ }),
809
+ z.object({
810
+ schemaVersion: z.literal(2),
811
+ generatedAt: z.string(),
812
+ channel: PluginMarketChannelSchema,
813
+ plugins: z.array(PluginMarketCatalogV2EntrySchema),
814
+ }),
815
+ ]);
710
816
  export type PluginMarketCatalog = z.infer<typeof PluginMarketCatalogSchema>;
711
817
 
712
818
  export const PluginMarketCatalogResponseSchema = z.object({