@international-iot-association/plugin-contracts 3.0.0-rc.1

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.
@@ -0,0 +1,1931 @@
1
+ declare const FIXTURE_LOGICAL = "serial.fixture";
2
+ declare const DUT_LOGICAL = "serial.dut";
3
+ declare const CURRENT_METER_LOGICAL = "instrument.current-meter";
4
+ declare const FIXTURE_BOARD_ADDRESS = 250;
5
+ declare const FIXTURE_BAUD_RATE = 115200;
6
+ declare const T9W_DUT_BAUD_RATE = 460800;
7
+ declare const KNOWN_T9W_FIXTURE_ENDPOINT = "58b20024743";
8
+ declare const KNOWN_T9W_DUT_ENDPOINT = "58b20024741";
9
+ interface FixturePortSelection {
10
+ fixture?: SerialPortInfo;
11
+ dut?: SerialPortInfo;
12
+ reasons: string[];
13
+ }
14
+ interface OperatorSignals {
15
+ keys: string[];
16
+ encoderForward: boolean;
17
+ encoderReverse: boolean;
18
+ }
19
+ declare function isFixtureReadyEvent(event: Record<string, unknown>): boolean;
20
+ declare function preferredSerialPortPath(path: string): string;
21
+ declare function allSerialPortPaths(port: SerialPortInfo): string[];
22
+ declare function serialEndpointId(path: string): string | null;
23
+ declare function serialPortHasEndpoint(port: SerialPortInfo, endpoint: string): boolean;
24
+ declare function preferredSerialPathFromPort(port: SerialPortInfo): string;
25
+ /**
26
+ * 粗筛「可能是 USB 转串口」的候选。
27
+ * - macOS:cu/tty.wchusbserial* / usbmodem*
28
+ * - Windows:COM* + CH340/CP210/FTDI 等元数据(path 本身没有 wch 字样)
29
+ * 协议已命中的口不应再被本函数否决——见 chooseFixtureAndDutPortPairs。
30
+ */
31
+ declare function looksLikeUsbSerialPort(port: SerialPortInfo): boolean;
32
+ interface FixtureDutPair {
33
+ fixture: SerialPortInfo;
34
+ dut: SerialPortInfo;
35
+ }
36
+ interface FixturePortPairSelection {
37
+ pairs: FixtureDutPair[];
38
+ reasons: string[];
39
+ }
40
+ interface ChooseFixtureDutOptions {
41
+ /**
42
+ * 需要的夹具+DUT 对数(多席位)。默认 1。
43
+ * 协议探测优先;不足时才考虑已知端点兜底。
44
+ */
45
+ count?: number;
46
+ /** 已占用路径(含别名),不再分配。 */
47
+ excludePaths?: string[];
48
+ /**
49
+ * 可选:已知夹具 USB 端点(仅作协议探测失败时的兜底,不区分产品型号)。
50
+ * 不传则不使用端点硬编码;传空字符串可显式关闭默认兜底。
51
+ */
52
+ fixtureEndpoint?: string | null;
53
+ /** 可选:已知 DUT 转发端点兜底。 */
54
+ dutEndpoint?: string | null;
55
+ /**
56
+ * @deprecated 逻辑名不再参与筛选;夹具以 `kind=fixture-modbus`(协议握手成功)为准。
57
+ * 保留字段以免旧调用方类型报错。
58
+ */
59
+ fixtureLogicalName?: string;
60
+ /** @deprecated 见 fixtureLogicalName。 */
61
+ dutLogicalName?: string;
62
+ /**
63
+ * true 时在协议结果之后仍尝试 KNOWN_T9W_* 端点兜底(默认 true,兼容旧实验室固定线序)。
64
+ * 跨机/跨 Windows 部署应依赖协议探测,不依赖该兜底。
65
+ */
66
+ allowKnownEndpointFallback?: boolean;
67
+ }
68
+ /**
69
+ * 选择夹具控制板 + DUT 转发口。
70
+ *
71
+ * **优先级(不区分 T9W/TRV 等产品型号)**:
72
+ * 1. Agent `detectDevices` 中 `kind=fixture-modbus`(0xFA Modbus 握手成功)→ 协议可用即夹具;
73
+ * 2. 同建议的 `pairedWith` / `role=dut` / 同族 USB 兄弟口 → DUT 转发;
74
+ * 3. 可选:已知 USB 端点硬编码(仅实验室线序兜底,默认开启以兼容旧台架)。
75
+ *
76
+ * 物理路径因 macOS/Windows/重插而变;跨机部署必须走 1+2,勿写死 path。
77
+ */
78
+ declare function chooseFixtureAndDutPorts(ports: SerialPortInfo[], suggestions?: SerialDeviceSuggestion[], options?: ChooseFixtureDutOptions): FixturePortSelection;
79
+ /**
80
+ * 多席位版本:按协议探测结果依次分配互不重叠的 fixture+DUT 对。
81
+ */
82
+ declare function chooseFixtureAndDutPortPairs(ports: SerialPortInfo[], suggestions?: SerialDeviceSuggestion[], options?: ChooseFixtureDutOptions): FixturePortPairSelection;
83
+ declare function buildFixtureSerialConfig(port: SerialPortInfo | undefined, options?: {
84
+ logicalName?: string;
85
+ boardAddress?: number;
86
+ }): SerialChannelConfig;
87
+ declare function buildDutRawTextSerialConfig(port: SerialPortInfo | undefined, options?: {
88
+ logicalName?: string;
89
+ baudRate?: number;
90
+ }): SerialChannelConfig;
91
+ declare function parseOperatorSignals(text: string): OperatorSignals;
92
+
93
+ /**
94
+ * ctx.channels.control() 是宿主进程返回给插件运行时的受控边界。
95
+ *
96
+ * 这些 helper 只做“运行时值 -> 契约类型”的窄化,不参与业务判断:业务代码仍然需要
97
+ * 检查数组是否为空、通道是否打开、端口是否满足设备策略。这样可以让示例插件少写重复
98
+ * 的类型断言,同时避免把具体工站流程塞进公共包。
99
+ */
100
+ declare function asSerialPorts(value: unknown): SerialPortInfo[];
101
+ declare function asSerialDeviceSuggestions(value: unknown): SerialDeviceSuggestion[];
102
+ declare function asChannelStatus(value: unknown): ChannelStatus;
103
+ /**
104
+ * writeRead() 的 response 在插件契约中故意保持 unknown:不同 provider/codec 会返回不同
105
+ * 结构。需要检查字段的示例插件先收敛成普通对象,再由业务逻辑读取具体字段。
106
+ */
107
+ declare function asResponseObject(value: unknown): Record<string, unknown>;
108
+
109
+ interface PluginStepDef {
110
+ key: string;
111
+ label: string;
112
+ description?: string;
113
+ }
114
+ /**
115
+ * 插件类别(详见 docs/global-plugins.md):
116
+ * - `station`(缺省):工位插件,由操作员按需启动。
117
+ * - `global`:全局能力插件(如日志、登录)。是否有 UI 与 kind 正交——
118
+ * `uiEntry` 省略即无界面后台插件。
119
+ */
120
+ type PluginKind = 'station' | 'global';
121
+ /**
122
+ * 插件 storage 作用域(W3-6,P0-06):
123
+ * - `version`(缺省):数据放在 `data/<pluginId>/versions/<version>/`,版本私有,
124
+ * 版本切换/回滚互不影响。
125
+ * - `shared`:数据放在 `data/<pluginId>/shared/`,跨版本共享。只有 manifest 显式
126
+ * 声明才使用;shared 数据的 schema 不得做破坏旧版本的不可逆迁移。
127
+ */
128
+ type PluginStorageScope = 'version' | 'shared';
129
+ /**
130
+ * manifest 的 storage 声明(W3-6)。缺省等价于
131
+ * `{ scope: 'version', migrateLegacy: true }`。
132
+ */
133
+ interface PluginStorageDeclaration {
134
+ scope?: PluginStorageScope;
135
+ /**
136
+ * false 时不把 W3-6 之前的平铺数据复制/移动进当前活动目录;旧文件原地保留,
137
+ * 适用于新版本不得继承历史敏感数据的迁移边界。缺省 true。
138
+ */
139
+ migrateLegacy?: boolean;
140
+ }
141
+ /** 插件的 manifest.json(即打包进插件 zip 内的清单)。 */
142
+ interface PluginManifest {
143
+ id: string;
144
+ name: string;
145
+ version: string;
146
+ description?: string;
147
+ /** 缺省视为 'station'。 */
148
+ kind?: PluginKind;
149
+ stationTypes: string[];
150
+ /**
151
+ * 仅为展示用元数据:外壳把它渲染成插件详情的「适用机型」/ 介绍页的「适配型号」,
152
+ * **不参与插件分发、准入或工位匹配判断**——那由 MES 侧 workOrder + stationType +
153
+ * skuId 解析出的 packages 清单决定。可省略(安装器归一化为 `[]`)。
154
+ */
155
+ models?: string[];
156
+ agentApi: string;
157
+ /**
158
+ * 插件 UI 入口。`kind: 'station'` 必填;`kind: 'global'` 可省略(无界面,
159
+ * 外壳不创建 iframe)。
160
+ */
161
+ uiEntry?: string;
162
+ runtimeEntry: string;
163
+ permissions: string[];
164
+ /** true 时外壳启动后自动拉起该插件(连同其依赖闭包,按拓扑序)。 */
165
+ autoStart?: boolean;
166
+ /**
167
+ * 插件依赖:被依赖插件 id -> 版本范围(支持 `*`、精确 `1.2.3`、`^1.2.3`、`1.x`)。
168
+ * 启动时依赖先于依赖者启动;同时它也是服务总线(轴 D)的调用授权 ACL。
169
+ */
170
+ dependencies?: Record<string, string>;
171
+ resultSchema?: string;
172
+ /**
173
+ * 仅为可选元数据。由于外壳通过 API-Bridge(而非通用的 runStep)驱动插件,
174
+ * `steps` 不再用于渲染外壳侧的按钮;保留它只是为了文档 / 注册表展示。
175
+ */
176
+ steps?: PluginStepDef[];
177
+ checksum?: string;
178
+ /**
179
+ * W3-6(P0-06):插件本地存储作用域声明。缺省为 version scope
180
+ * (`data/<pluginId>/versions/<version>/`)。注意外壳安装器归一化后的
181
+ * InstalledPlugin.manifest 可能不携带该字段——外壳按包内 manifest.json 原文解析。
182
+ */
183
+ storage?: PluginStorageDeclaration;
184
+ /**
185
+ * W3-4(P0-06):readiness(activate)等待超时(毫秒)。版本切换事务缺省 8000ms,
186
+ * 超时按失败处理并恢复原版本。readiness 必须是无副作用检查:activate 只允许
187
+ * 模块加载/配置解析/本地依赖检查,不允许写 SN、切继电器、烧录等硬件动作。
188
+ */
189
+ readinessTimeoutMs?: number;
190
+ }
191
+ /** 注册表服务器对外公布的一个安装包。 */
192
+ interface RemotePlugin {
193
+ id: string;
194
+ name: string;
195
+ version: string;
196
+ description?: string;
197
+ kind?: PluginKind;
198
+ stationTypes: string[];
199
+ /**
200
+ * 展示用元数据,语义同 {@link PluginManifest.models}。注册表侧仍为必填
201
+ * (后端登记记录始终携带该键,解析时 fail-closed 校验),外壳只用于渲染。
202
+ */
203
+ models: string[];
204
+ agentApi: string;
205
+ permissions: string[];
206
+ autoStart?: boolean;
207
+ dependencies?: Record<string, string>;
208
+ /** 后端历史 JSON 登记记录可能未知;安装时仍以实际下载字节执行上限与验签。 */
209
+ sizeBytes: number | null;
210
+ sha256: string;
211
+ /**
212
+ * Ed25519 分离签名(base64):对 {agentApi,id,sha256,sizeBytes,version}
213
+ * 规范化 JSON(键排序)的签名,由内部发布链 root key 或被 root 背书的
214
+ * publisher key 签出。生产 MES 上传可为空串;development 未配置信任根时也可为空。
215
+ */
216
+ signature: string;
217
+ downloadUrl: string;
218
+ }
219
+ /** 签名 registry 中被 root key 背书的 publisher 公钥(支持 key rotation)。 */
220
+ interface RegistryPublisherKey {
221
+ /** 公钥指纹:sha256(SPKI DER) 前 16 个 hex 字符。 */
222
+ keyId: string;
223
+ publicKeyPem: string;
224
+ /** root key 对 {keyId, publicKeyPem} 规范化 JSON 的 Ed25519 签名(base64)。 */
225
+ signature: string;
226
+ }
227
+ /** registry.json 顶层的签名快照(对整个发布集合的完整性签名)。 */
228
+ interface RegistrySnapshot {
229
+ /** root key 对快照签名内容(含全部包签名内容、撤销列表等)的签名(base64)。 */
230
+ signature: string;
231
+ rootKeyId: string;
232
+ }
233
+ /**
234
+ * registry 文档(demo/生产 registry 服务对外壳提供的完整形态)。
235
+ * 新增字段全部可选,向后兼容旧的「纯数组」响应。
236
+ */
237
+ interface PluginRegistryDocument {
238
+ generatedAt: string;
239
+ plugins: RemotePlugin[];
240
+ snapshot?: RegistrySnapshot;
241
+ /** 已确认供应链/安全事故的包 sha256 集合:安装与启动一律拒绝,但不自动删除。 */
242
+ revokedDigests?: string[];
243
+ /** pluginId -> 允许安装的最低版本(strict SemVer)。 */
244
+ minVersions?: Record<string, string>;
245
+ publisherKeys?: RegistryPublisherKey[];
246
+ }
247
+ /** 包签名状态(外壳安装器判定)。 */
248
+ type PluginSignatureState = 'verified' | 'unsigned-dev' | 'local-dev'
249
+ /** Shell 安装包 extraResources 白名单底座插件,经清单 sha256 证明后落盘。 */
250
+ | 'bundled-attested'
251
+ /** 经认证 MES 精确版本 download-url 安装的已发布包。 */
252
+ | 'mes-attested'
253
+ /** 经认证 MES 下载的 draft;启动前需用户允许。 */
254
+ | 'mes-draft';
255
+ /** 已通过校验、解压并在本地登记的插件。 */
256
+ interface InstalledPlugin {
257
+ id: string;
258
+ version: string;
259
+ manifest: PluginManifest;
260
+ installedAt: string;
261
+ installDir: string;
262
+ sha256: string;
263
+ /**
264
+ * 签名判定结果:`verified` = 已按信任根验签通过;`mes-attested` =
265
+ * 经生产 MES 官方渠道安装的 released 包;`mes-draft` = MES draft;
266
+ * `unsigned-dev` = development 未验签;`local-dev` = 本地目录开发安装;
267
+ * `bundled-attested` = 安装包白名单底座。缺省视为签名机制之前的历史安装。
268
+ * production 对非官方包改为启动前提示后允许,不再直接拒绝。
269
+ */
270
+ signatureState?: PluginSignatureState;
271
+ }
272
+ /** 本地保留的某个插件历史版本目录。 */
273
+ interface PluginVersionHistoryEntry {
274
+ id: string;
275
+ version: string;
276
+ installDir: string;
277
+ active: boolean;
278
+ installedAt?: string;
279
+ sha256?: string;
280
+ /** W3-4:安装目录占用字节数(供版本下拉/清理 UI 展示;计算失败时缺省)。 */
281
+ sizeBytes?: number;
282
+ /** W3-4:该版本落盘时的验签判定(缺省 = 引入签名机制之前的历史安装)。 */
283
+ signatureState?: PluginSignatureState;
284
+ }
285
+ /** 清理非活动历史版本后的结果。 */
286
+ interface PluginVersionCleanupResult {
287
+ pluginId: string;
288
+ activeVersion?: string;
289
+ keptVersions: string[];
290
+ removedVersions: string[];
291
+ /**
292
+ * W3-6:随被删版本一并删除的 version-local storage
293
+ * (`data/<pluginId>/versions/<version>/`)字节数合计。清理确认文案属 UI 层,
294
+ * 本层只提供数据。
295
+ */
296
+ removedDataBytes?: number;
297
+ }
298
+ /** 版本下拉里的一个本地版本选项(15 章第 10 节的数据面)。 */
299
+ interface PluginVersionOption {
300
+ version: string;
301
+ sha256?: string;
302
+ installedAt?: string;
303
+ sizeBytes?: number;
304
+ signatureState?: PluginSignatureState;
305
+ /** installed-registry 当前登记的 active 版本(与 selected 保持同步的落盘态)。 */
306
+ active: boolean;
307
+ /** 该版本 runtime 正在运行。 */
308
+ running: boolean;
309
+ /** 持久化 pin(下次启动/当前期望运行的版本)。 */
310
+ selected: boolean;
311
+ /** 最近一次通过 readiness 且被明确维护确认的版本。 */
312
+ lastKnownGood: boolean;
313
+ /**
314
+ * 非空 = 该版本不可被切换启用,并给出明确原因(agentApi 不兼容 / revoked
315
+ * digest / 未验签等)。UI 应展示为 disabled 并在 tooltip 中说明。
316
+ */
317
+ disabledReasons: string[];
318
+ }
319
+ /** 最近一次 registry 刷新后可见的远端版本(供详情页逐版本下载)。 */
320
+ interface RemotePluginVersionOption {
321
+ version: string;
322
+ sha256: string;
323
+ sizeBytes: number | null;
324
+ description?: string;
325
+ status: 'draft' | 'released' | 'offline' | 'rolled_back';
326
+ /** 后端返回的下载授权;Shell 不得从生命周期自行推导。 */
327
+ downloadable: boolean;
328
+ /** 不可下载时供可信 Shell UI 展示的固定原因;可下载时为 null。 */
329
+ downloadDisabledReason: string | null;
330
+ publishedAt?: string | null;
331
+ availableLocally: boolean;
332
+ selected: boolean;
333
+ running: boolean;
334
+ latest: boolean;
335
+ }
336
+ /**
337
+ * 单个插件的聚合版本状态(P0-06 状态模型)。五个字段语义分离,
338
+ * 不再用一个 `installed.version` 同时表示「磁盘上有 / 已选择 / 正在运行」。
339
+ */
340
+ interface PluginVersionState {
341
+ pluginId: string;
342
+ /** 本地已验签保留的版本集合(按版本降序)。 */
343
+ availableVersions: PluginVersionOption[];
344
+ /** 最近一次 registry 刷新后缓存的远端版本集合(按版本降序)。 */
345
+ remoteVersions: RemotePluginVersionOption[];
346
+ /** 远端查询失败时保留本地版本,并由 UI 展示刷新失败。 */
347
+ remoteStatus: 'ready' | 'unavailable';
348
+ /** 持久化 pin;缺省 = 当前 active 版本。 */
349
+ selectedVersion: string | null;
350
+ /** 当前 runtime 实际运行的版本;停止时为 null(运行期派生,不落盘)。 */
351
+ runningVersion: string | null;
352
+ /** 最近通过 readiness 且被明确确认的版本(confirmVersionGood)。 */
353
+ lastKnownGoodVersion: string | null;
354
+ /** registry 中可下载的更高版本(listRemote 后缓存;不自动下载/启用)。 */
355
+ registryCandidate: string | null;
356
+ }
357
+ type RuntimeState = 'stopped' | 'starting' | 'running' | 'error';
358
+ interface RuntimeStatus {
359
+ pluginId: string;
360
+ state: RuntimeState;
361
+ startedAt?: string;
362
+ lastError?: string;
363
+ }
364
+ interface RuntimeLogEntry {
365
+ ts: string;
366
+ level: 'info' | 'warn' | 'error' | 'debug';
367
+ pluginId: string;
368
+ message: string;
369
+ /** 可选模块标签(统一日志门面 tag)。 */
370
+ tag?: string;
371
+ /** 可选结构化字段(ADR-0002)。 */
372
+ data?: Record<string, unknown>;
373
+ }
374
+ /** 单个测试步骤的结果,与 schemas/result.schema.json 对应。 */
375
+ interface StepResult {
376
+ ok: boolean;
377
+ stepKey: string;
378
+ measurements?: Record<string, unknown>;
379
+ raw?: string;
380
+ error?: string;
381
+ startedAt: string;
382
+ finishedAt: string;
383
+ }
384
+ /** 运行时上报的中间进度事件。 */
385
+ interface StepEvent {
386
+ pluginId: string;
387
+ stepKey: string;
388
+ phase?: string;
389
+ message: string;
390
+ ts: string;
391
+ }
392
+ interface StationPaths {
393
+ root: string;
394
+ packages: string;
395
+ installed: string;
396
+ /** 插件私有数据根目录(ctx.storage 的宿主侧根,按 pluginId 分目录)。 */
397
+ data: string;
398
+ }
399
+ /** 标准 IPC 信封,使渲染进程无需捕获原始抛出的异常。 */
400
+ interface UnofficialPluginRunPrompt {
401
+ pluginId: string;
402
+ pluginName: string;
403
+ version: string;
404
+ sha256: string;
405
+ kind: 'unofficial' | 'draft';
406
+ }
407
+ type IpcResult<T> = {
408
+ ok: true;
409
+ data: T;
410
+ } | {
411
+ ok: false;
412
+ error: string;
413
+ code?: string;
414
+ unofficialRun?: UnofficialPluginRunPrompt;
415
+ };
416
+ interface SerialPortInfo {
417
+ path: string;
418
+ aliases?: string[];
419
+ manufacturer?: string;
420
+ serialNumber?: string;
421
+ vendorId?: string;
422
+ productId?: string;
423
+ pnpId?: string;
424
+ locationId?: string;
425
+ friendlyName?: string;
426
+ /**
427
+ * 该口是外壳虚拟串口能力铸出的 PTY(ADR-0031 D6 选项 A),不是物理设备。
428
+ *
429
+ * 只在外壳「设置 → 开发者 → 虚拟串口」开着时出现,且只会是客户端那一端。
430
+ * 它是**枚举**结果:外壳不做自动绑定,路径仍要人工选中 / 抄写。
431
+ * 缺省 undefined = 真实口。
432
+ *
433
+ * 插件可据此加角标或拒绝用于放行;`manufacturer` / `friendlyName` 里也带着
434
+ * 中文标记,两处一致。
435
+ */
436
+ virtual?: boolean;
437
+ /**
438
+ * **同一块板上的多个串口共享的板级标识**;判不出同板归属时缺省 undefined。
439
+ *
440
+ * 真实 USB 双串口板靠**设备级** `serialNumber` 表达「这两个口是一块板」
441
+ * (同一 `IOUSBHostDevice` 下的两个接口读到同一个值,见
442
+ * `trv-wifi-combined/runtime/rf/station/fixture-candidates.ts` 的 `serialBoardKey`)。
443
+ * 虚拟 PTY 没有 USB 描述符,这条路走不通——于是改由**铸口方显式声明**:
444
+ * 仿真插件铸一席的夹具口与 DUT 口时传同一个 `boardId`,外壳原样投影。
445
+ *
446
+ * 这不是自动发现(ADR-0031 D6 删掉的是「描述符一活、真实端口整份被顶替」
447
+ * 那套机制):声明的是**设备自己的身份**,不是「这条口属于第几席」。
448
+ * 席位归属仍然只由夹具下压产生(`serial-seat-binding.ts`)。
449
+ */
450
+ boardId?: string;
451
+ }
452
+ type SerialProbeKind = 'fixture-modbus' | 'esp32-json' | 'unknown' | 'ignored';
453
+ type SerialPortRole = 'fixture' | 'dut' | 'simulator' | 'unknown' | 'ignored';
454
+ type SerialPortCapability = 'modbus-rtu' | 'fixture-ready-event' | 'fixture-power-control' | 'raw-text' | 'json-lines';
455
+ interface SerialProbeAttempt {
456
+ kind: SerialProbeKind;
457
+ ok: boolean;
458
+ message: string;
459
+ latencyMs?: number;
460
+ response?: unknown;
461
+ }
462
+ interface SerialDeviceSuggestion {
463
+ port: SerialPortInfo;
464
+ kind: SerialProbeKind;
465
+ role: SerialPortRole;
466
+ capabilities: SerialPortCapability[];
467
+ logicalName?: string;
468
+ baudRate?: number;
469
+ pairedWith?: string;
470
+ confidence: number;
471
+ reasons: string[];
472
+ attempts: SerialProbeAttempt[];
473
+ }
474
+ interface JsonLinesCodecConfig {
475
+ type: 'json-lines';
476
+ delimiter: '\n';
477
+ maxFrameBytes: number;
478
+ ignoreUntilJson?: boolean;
479
+ }
480
+ interface ModbusRtuCodecConfig {
481
+ type: 'modbus-rtu';
482
+ deviceAddress?: number;
483
+ maxFrameBytes: number;
484
+ }
485
+ interface RawTextCodecConfig {
486
+ type: 'raw-text';
487
+ delimiter?: '\n' | '\r\n' | '\r';
488
+ maxFrameBytes: number;
489
+ encoding?: 'utf8' | 'latin1';
490
+ appendDelimiter?: boolean;
491
+ }
492
+ type SerialCodecConfig = JsonLinesCodecConfig | ModbusRtuCodecConfig | RawTextCodecConfig;
493
+ interface ScpiTextCodecConfig {
494
+ type: 'scpi-text';
495
+ delimiter?: '\n' | '\r\n' | '\r';
496
+ maxFrameBytes: number;
497
+ encoding?: 'utf8' | 'latin1';
498
+ appendDelimiter?: boolean;
499
+ }
500
+ interface SerialChannelConfig {
501
+ logicalName: string;
502
+ provider: 'serial';
503
+ /** 可显式引用连接 profile;配置 channelBindings 后通常无需由插件传入。 */
504
+ profileRef?: string;
505
+ /** 兼容本地 bring-up;生产应由 Shell station profile 的 channelBindings 注入。 */
506
+ port?: string;
507
+ baudRate: number;
508
+ dataBits?: 7 | 8;
509
+ stopBits?: 1 | 2;
510
+ parity?: 'none' | 'even' | 'odd';
511
+ codec: SerialCodecConfig;
512
+ timeouts: {
513
+ openMs: number;
514
+ responseMs: number;
515
+ idleFrameMs?: number;
516
+ };
517
+ signals?: {
518
+ dtr?: boolean;
519
+ rts?: boolean;
520
+ };
521
+ openStabilizeMs?: number;
522
+ }
523
+ interface TcpChannelConfig {
524
+ logicalName: string;
525
+ provider: 'tcp';
526
+ /**
527
+ * W3-2:受保护 station profile 的引用。仪表的 host/port 由外壳按 profileRef
528
+ * 从 station-profile.json 注入——非回环 endpoint 不得内联(外壳 fail-closed
529
+ * 拒绝),除非声明 `endpointSource: 'mes-machine-instruments'`。
530
+ * 插件只提供非敏感业务字段(codec/timeouts/keepAlive 等)。
531
+ */
532
+ profileRef?: string;
533
+ /**
534
+ * 仪表地址。允许来源:
535
+ * - 回环(127.x/localhost/::1)本地 mock;
536
+ * - `endpointSource: 'mes-machine-instruments'` 时 MES 下发的产线 IP;
537
+ * - 其它非回环一律走 profileRef(内联会被外壳拒绝)。
538
+ */
539
+ host?: string;
540
+ /** TCP 端口。走 profileRef 时由 station profile 注入,可省略。 */
541
+ port?: number;
542
+ /**
543
+ * 端点权威声明。取值 `'mes-machine-instruments'` 时,instrument.* tcp 通道
544
+ * 可内联非回环 host/port(须先经 platform.mes 拉取 machine-instruments)。
545
+ * 外壳解析后剥离,不透传 Agent provider。
546
+ */
547
+ endpointSource?: 'mes-machine-instruments';
548
+ codec: ScpiTextCodecConfig;
549
+ timeouts: {
550
+ connectMs: number;
551
+ responseMs: number;
552
+ idleFrameMs?: number;
553
+ };
554
+ keepAlive?: boolean;
555
+ openStabilizeMs?: number;
556
+ }
557
+ /**
558
+ * 逻辑 MQTT broker 通道的物理映射。writeRead 的 payload 是 MQTT 操作:
559
+ * `{ op: 'subscribe' | 'unsubscribe', topics }` / `{ op: 'publish', topic,
560
+ * payload, qos?, retain? }`;broker 推送的 PUBLISH 以 unsolicited channelEvent
561
+ * 形式上抛(`{ event: 'mqtt-message', topic, payload }`),用 ctx.channels.subscribe 接收。
562
+ */
563
+ interface MqttChannelConfig {
564
+ logicalName: string;
565
+ provider: 'mqtt';
566
+ /**
567
+ * W2-7:受保护 station profile 的引用。broker 的 host/port/username/password
568
+ * 由外壳按 profileRef 从 station-profile.json 注入——插件源码/插件包内不得携带
569
+ * 这些字段(外壳 fail-closed 拒绝内联 endpoint/凭据)。插件只提供非敏感业务
570
+ * 字段(clientId / keepaliveSec / timeouts)。
571
+ */
572
+ profileRef?: string;
573
+ /** broker 地址。仅本地调试直连可用;生产一律走 profileRef(内联会被外壳拒绝)。 */
574
+ host?: string;
575
+ port?: number;
576
+ clientId: string;
577
+ /** 已废弃的内联凭据入口:外壳一律拒绝,凭据只能来自 station profile。 */
578
+ username?: string;
579
+ password?: string;
580
+ /** MQTT keepalive 心跳间隔(秒,默认 30)。 */
581
+ keepaliveSec?: number;
582
+ timeouts: {
583
+ connectMs: number;
584
+ responseMs: number;
585
+ };
586
+ }
587
+ interface MockChannelConfig {
588
+ logicalName: string;
589
+ provider: 'mock';
590
+ }
591
+ /**
592
+ * S3 兼容对象存储上传通道(如阿里云 OSS)。writeRead 的 payload 是一次性操作:
593
+ * `{ op: 'putObject', key, contentBase64, contentType? }` → `{ ok, url, md5, size, key }`。
594
+ * 真正的 HTTP PUT 由宿主进程执行(宿主可出网),md5/size 也由宿主计算;沙箱内的
595
+ * 插件不直接触网。
596
+ *
597
+ * W2-7:endpoint/bucket/凭据一律来自受保护 station profile(profileRef 引用),
598
+ * 插件不得内联——外壳 fail-closed 拒绝携带这些字段的 configure 请求。
599
+ */
600
+ interface OssChannelConfig {
601
+ logicalName: string;
602
+ provider: 'oss';
603
+ /** 受保护 station profile 的引用(endpoint/region/bucket/baseUrl/凭据由外壳注入)。 */
604
+ profileRef?: string;
605
+ /** 以下字段仅保留给本地调试直连;生产一律走 profileRef(内联会被外壳拒绝)。 */
606
+ endpoint?: string;
607
+ region?: string;
608
+ bucket?: string;
609
+ /** 拼接返回 URL 用的公开基址,如 https://<bucket>.<region>.aliyuncs.com */
610
+ baseUrl?: string;
611
+ accessKeyId?: string;
612
+ secretAccessKey?: string;
613
+ }
614
+ type DeviceChannelConfig = SerialChannelConfig | TcpChannelConfig | MqttChannelConfig | OssChannelConfig | MockChannelConfig;
615
+ type ChannelState = 'unconfigured' | 'closed' | 'opening' | 'open' | 'error';
616
+ interface ChannelMetrics {
617
+ bytesRx: number;
618
+ bytesTx: number;
619
+ requestCount: number;
620
+ successCount: number;
621
+ timeoutCount: number;
622
+ parseErrorCount: number;
623
+ reopenCount: number;
624
+ queueDepth: number;
625
+ latencyMsP50: number;
626
+ latencyMsP95: number;
627
+ latencyMsMax: number;
628
+ }
629
+ interface ChannelStatus {
630
+ logicalName: string;
631
+ provider: 'serial' | 'tcp' | 'mqtt' | 'oss' | 'mock';
632
+ state: ChannelState;
633
+ /** serial provider 的规范化物理资源 ID;共享 binding 的 metrics 均按该资源累计。 */
634
+ resourceId?: string;
635
+ port?: string;
636
+ baudRate?: number;
637
+ host?: string;
638
+ tcpPort?: number;
639
+ lastError?: string;
640
+ metrics: ChannelMetrics;
641
+ }
642
+ type SerialUsageState = 'intent' | 'open' | 'open-failed';
643
+ /** 一条使用记录:某插件 runtime 通过某 logical name 使用某物理资源。 */
644
+ interface SerialUsageUser {
645
+ pluginId: string;
646
+ /** 该插件 runtime 实例的 ID(外壳每次启动 runtime 生成,插件不可伪造)。 */
647
+ runtimeInstanceId: string;
648
+ logicalName: string;
649
+ /**
650
+ * intent = configure 成功(使用意图);open = 打开成功的 owner;
651
+ * open-failed = configure 冲突或 open 失败的 contender(持续保留显示冲突原因)。
652
+ */
653
+ state: SerialUsageState;
654
+ /** 规范化连接配置(剔除凭据字段)的 sha256——profile 不同即 hash 不同。 */
655
+ connectionProfileHash: string;
656
+ openedAt?: string;
657
+ errorCode?: string;
658
+ }
659
+ /**
660
+ * 一个物理资源的使用快照(15 章 P0-05 数据模型)。resourceId 为规范化物理资源
661
+ * key(serial 别名已合并,如 `serial:endpoint:58b20024743`);conflict 只对
662
+ * serial 类资源判定:去重后的非 shell-debug pluginId 数量 > 1。平台只观测告警,
663
+ * 不自动停止、不抢占、不策略性拒绝(B3/D02)。
664
+ */
665
+ interface SerialUsageSnapshot {
666
+ resourceId: string;
667
+ provider: 'serial' | 'tcp' | 'mqtt' | 'oss';
668
+ users: SerialUsageUser[];
669
+ conflict: boolean;
670
+ }
671
+ /**
672
+ * 通过单一通道将事件从主进程推送到渲染进程。
673
+ *
674
+ * `uiMessage` 携带一个 OPAQUE(不透明)的 api-bridge 信封,从插件运行时回传到
675
+ * 插件 UI 的 iframe(轴 A)。外壳从不检查 `envelope`。
676
+ */
677
+ type RuntimeEvent = {
678
+ kind: 'status';
679
+ status: RuntimeStatus;
680
+ } | {
681
+ kind: 'log';
682
+ log: RuntimeLogEntry;
683
+ } | {
684
+ kind: 'stepEvent';
685
+ event: StepEvent;
686
+ } | {
687
+ kind: 'stepResult';
688
+ pluginId: string;
689
+ result: StepResult;
690
+ /**
691
+ * W4-4(P0-07 第 10 条):production 模式下,1.x(legacy)插件的 report
692
+ * 结果若其 exact digest 未通过 legacy verdict allowlist 审核,外壳标注
693
+ * legacy-unverified。该结果仍进 UI 记录面板,但绝不进入正式 outbox
694
+ * ——1.x 结果本就没有 run 生命周期,进入正式 MES 的唯一途径是未来经
695
+ * legacy adapter 显式桥接 + allowlist 审核(当前不桥接)。
696
+ */
697
+ legacyUnverified?: true;
698
+ } | {
699
+ kind: 'uiMessage';
700
+ pluginId: string;
701
+ envelope: unknown;
702
+ }
703
+ /** 主进程请求 renderer 聚焦某个插件(例如后台一键就绪完成后)。 */
704
+ | {
705
+ kind: 'focusPlugin';
706
+ requestId: string;
707
+ pluginId: string;
708
+ version?: string;
709
+ source: 'machine-ready';
710
+ }
711
+ /** Auth runtime 只请求 Shell 打开可信凭据对话框;token 不进入插件进程。 */
712
+ | {
713
+ kind: 'credentialConfigurationRequested';
714
+ pluginId: string;
715
+ }
716
+ /** 安装确认只在 Shell renderer 的可信 UI 展示;插件 iframe 不能代替操作员确认。 */
717
+ | {
718
+ kind: 'pluginInstallConfirmationRequested';
719
+ request: PluginInstallConfirmationRequest;
720
+ } | {
721
+ kind: 'unofficialRunConfirmationRequested';
722
+ prompt: UnofficialPluginRunPrompt;
723
+ }
724
+ /**
725
+ * 开发闭环(development profile):local-dev 插件的来源目录构建产物
726
+ * (runtime-dist / ui-dist / manifest.json)发生变化。调试页据此提示
727
+ * "产物已更新,点击重载生效"——刻意不自动重启(runtime 可能正持有串口/夹具)。
728
+ */
729
+ | {
730
+ kind: 'localDevSourceChanged';
731
+ pluginId: string;
732
+ };
733
+
734
+ /**
735
+ * 轴 A(插件 UI <-> 插件运行时)的传输层。外壳负责传递不透明信封;
736
+ * @international-iot-association/plugin-sdk 在其之上叠加一个 API-Bridge 服务端。
737
+ */
738
+ interface PluginUiTransport {
739
+ /** 向插件 UI 的 iframe 发送一个不透明信封。 */
740
+ postMessage(envelope: unknown): void;
741
+ /** 注册一个处理器,用于接收来自插件 UI iframe 的不透明信封。 */
742
+ onMessage(handler: (envelope: unknown) => void): void;
743
+ }
744
+ /**
745
+ * 轴 D(插件 runtime <-> 插件 runtime)的服务总线。与轴 A 同构:外壳只按
746
+ * from/to 路由不透明信封,永不解析内容。路由授权规则(外壳强制):
747
+ * A -> B 允许当且仅当 B ∈ A.dependencies 或 A ∈ B.dependencies。
748
+ * 请使用 @international-iot-association/plugin-sdk 的 createServiceClient / createServiceServer,
749
+ * 不要直接操作本传输层。详见 docs/global-plugins.md。
750
+ */
751
+ interface PluginServiceBus {
752
+ /** 向目标插件 runtime 发送一个不透明信封(外壳校验路由权限,违规丢弃并记日志)。 */
753
+ postMessage(targetPluginId: string, envelope: unknown): void;
754
+ /** 接收来自其他插件的信封;`from` 为对端 pluginId,由外壳注入、不可伪造。 */
755
+ onMessage(handler: (from: string, envelope: unknown) => void): void;
756
+ }
757
+ /**
758
+ * 插件私有本地存储(需在 manifest.permissions 声明 `"storage"`,否则
759
+ * ctx.storage 为 undefined)。相对路径不得逃逸(宿主强制包含检查)。缺省根目录为
760
+ * `<data>/<pluginId>/versions/<version>/`;只有显式 `storage.scope: "shared"` 才使用
761
+ * `<data>/<pluginId>/shared/` 跨版本保留数据。
762
+ */
763
+ interface PluginStorage {
764
+ /** 数据目录绝对路径(仅供展示/日志,读写请走下面的 API)。 */
765
+ readonly dir: string;
766
+ /** 读取文本文件;不存在返回 null。 */
767
+ read(relPath: string): Promise<string | null>;
768
+ /** 写入(覆盖)文本文件;自动创建父目录。 */
769
+ write(relPath: string, content: string): Promise<void>;
770
+ /** 追加文本;文件/父目录不存在时自动创建。 */
771
+ append(relPath: string, content: string): Promise<void>;
772
+ /** 列出目录下的文件名(不含子目录内容);目录不存在返回 []。 */
773
+ list(relDir?: string): Promise<string[]>;
774
+ /** 删除文件;不存在时静默成功。 */
775
+ remove(relPath: string): Promise<void>;
776
+ }
777
+ /** durable receipt:平台落盘(fsync)成功后才返回({runId, sequence, persistedAt})。 */
778
+ interface PlatformDurableReceipt {
779
+ runId: string;
780
+ /** begin=0;每条 step 为其 sequence;terminal 记录复用最后 sequence。 */
781
+ sequence: number;
782
+ persistedAt: string;
783
+ }
784
+ /** 一次产测 run 的被测对象(SN/工单/机型,均可选)。 */
785
+ interface PlatformRunSubject {
786
+ sn?: string;
787
+ workOrderId?: string;
788
+ model?: string;
789
+ }
790
+ /** ctx.platform.appendStep 的入参(sequence 由平台分配,插件不提供)。 */
791
+ interface PlatformRunStep {
792
+ stepKey: string;
793
+ /** 同一步骤的第几次尝试(缺省 1)。 */
794
+ attempt?: number;
795
+ ok: boolean;
796
+ measurements?: Record<string, unknown>;
797
+ raw?: string;
798
+ error?: string;
799
+ startedAt: string;
800
+ finishedAt: string;
801
+ }
802
+ /**
803
+ * 平台盖章后的 run 归属模式:只有纯 production 平台模式才产生 production run;
804
+ * maintenance-simulated / development 一律 simulated。
805
+ * completeRun 仅本地 journal,不上传 MES envelope。
806
+ */
807
+ type PlatformRunResultMode = 'production' | 'simulated';
808
+ /**
809
+ * MES 代发 HTTP 方法(agentApi 3.0 + manifest `mes:api`)。
810
+ * 插件只传相对 path;token/指纹由 Shell 附加,绝不回传插件。
811
+ */
812
+ type MesHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
813
+ /** query 参数值:Shell 转为字符串并 encode;禁止嵌套对象/数组。 */
814
+ type MesParamValue = string | number | boolean;
815
+ /** 单次 MES 代发选项。默认 delivery=live。 */
816
+ interface MesRequestOptions {
817
+ /** query 键值;不得写在 path 字符串内。 */
818
+ params?: Record<string, MesParamValue>;
819
+ /**
820
+ * live:立即代发 MES。
821
+ * outbox:落盘入队(仅写方法);await 只表示已入队。
822
+ */
823
+ delivery?: 'live' | 'outbox';
824
+ /**
825
+ * 目标平台 Run id。
826
+ * - 无 active run 时:普通 live 可省略;显式 runId 因无法命中 active set 而拒绝;
827
+ * outbox 仍要求显式且有效的 runId。
828
+ * - 单 active run 时:live/outbox 均可省略以兼容旧代码。
829
+ * - 多 active run 时:outbox 必须显式提供且命中当前 active set;
830
+ * live 可省略(不归属任何 Run,用工位当前 mode),显式 runId 仍必须命中 active set。
831
+ */
832
+ runId?: string;
833
+ /**
834
+ * 本次 live 代发的单次超时(毫秒)。省略时用 Shell 全局默认(60s)。
835
+ *
836
+ * 为什么需要它:插件若想在应用层做「失败重试 N 次」,单次超时就必须由插件
837
+ * 定;否则 N × 全局 60s 会把一次操作卡上好几分钟。有了它,超时只有一个闸
838
+ * (Shell 侧),插件不必再叠一层 `Promise.race`——那样 Shell 的 fetch 不会被
839
+ * 取消,只会留一串悬挂请求占住代发并发槽。
840
+ *
841
+ * 约束:
842
+ * - Shell 钳制到 `[1s, 60s]`,**只能调小不能调大**;非法值(0/负数/NaN/
843
+ * 非 number)一律回落全局默认,不报错。
844
+ * - **只作用于 `delivery:'live'`**。outbox 排水是 Shell 后台自己的节奏,
845
+ * 入队记录也不持久化本字段,排水一律用全局默认。
846
+ * - 旧 Shell(不认识本字段)会在逐字段透传时**静默丢弃**它,超时退回 60s。
847
+ * 本字段不参与 agentApi 能力协商,插件无法探测对端是否支持。
848
+ */
849
+ timeoutMs?: number;
850
+ }
851
+ /** live 代发结果:HTTP 已结束;4xx/5xx 不抛。 */
852
+ interface MesLiveResult {
853
+ kind: 'live';
854
+ status: number;
855
+ data: unknown;
856
+ }
857
+ /** outbox 入队结果:已落盘;不等于 MES 已过站。 */
858
+ interface MesQueuedResult {
859
+ kind: 'queued';
860
+ receiptId: string;
861
+ runId: string;
862
+ enqueuedAt: string;
863
+ }
864
+ type MesResult = MesLiveResult | MesQueuedResult;
865
+ /**
866
+ * agentApi 3.0 + `mes:api`:path 形态 MES 客户端(挂在 ctx.platform.mes)。
867
+ * get/delete 禁止 body;post/put/patch body 可省略,若传则 JSON 序列化。
868
+ */
869
+ interface PluginPlatformMes {
870
+ get(path: string, options?: MesRequestOptions): Promise<MesResult>;
871
+ post(path: string, body?: unknown, options?: MesRequestOptions): Promise<MesResult>;
872
+ put(path: string, body?: unknown, options?: MesRequestOptions): Promise<MesResult>;
873
+ patch(path: string, body?: unknown, options?: MesRequestOptions): Promise<MesResult>;
874
+ delete(path: string, options?: MesRequestOptions): Promise<MesResult>;
875
+ }
876
+ /**
877
+ * @deprecated agentApi 2.x 具名 operation 面;3.0 已删除,勿在新代码使用。
878
+ * 保留类型名仅避免历史文档引用炸类型搜索;运行时不再注入。
879
+ */
880
+ interface PluginPlatformHttp {
881
+ invokeOperation(operation: string, params?: Record<string, string>, body?: unknown): Promise<{
882
+ status: number;
883
+ body: unknown;
884
+ }>;
885
+ }
886
+ /** agentApi 2.1.0 向插件公开的最小机器凭据状态;不包含 token 或 token 尾号。 */
887
+ interface PluginCredentialStatus {
888
+ state: PlatformCredentialStatus['state'];
889
+ environmentTag?: string;
890
+ verifiedAt?: string;
891
+ }
892
+ /**
893
+ * agentApi 2.1.0 的机器凭据窄接口。配置请求只让 Shell renderer 打开可信
894
+ * 对话框,token 输入值绝不经过 iframe/runtime。
895
+ */
896
+ interface PluginPlatformCredential {
897
+ getStatus(): Promise<PluginCredentialStatus>;
898
+ requestConfiguration(): Promise<void>;
899
+ }
900
+ /** agentApi 2.2.0:插件只能请求安装的包标识;安全元数据由 Shell 重新取得。 */
901
+ interface PluginInstallPackageRequest {
902
+ packageId: string;
903
+ version: string;
904
+ }
905
+ interface PluginInstallRequestInput {
906
+ /** 工单加载器必须携带;Shell 会重新解析工单并校验 packageId/version 子集。 */
907
+ workOrder?: string;
908
+ packages: PluginInstallPackageRequest[];
909
+ }
910
+ type PluginInstallProgressStatus = 'pending' | 'confirmed' | 'rejected' | 'downloading' | 'installed' | 'failed';
911
+ /** 单包安装状态;reason 必须是脱敏文案,不得包含 token 或预签名下载地址。 */
912
+ interface PluginInstallProgress {
913
+ requestId: string;
914
+ packageId: string;
915
+ version: string;
916
+ status: PluginInstallProgressStatus;
917
+ reason?: string;
918
+ /** 已安装同版本时为 true;此时不会重新下载。 */
919
+ skipped?: boolean;
920
+ }
921
+ interface PluginInstallRequestResult {
922
+ requestId: string;
923
+ packages: PluginInstallProgress[];
924
+ }
925
+ /** Shell 可信确认对话框使用的安全摘要;不包含 token、下载 URL 或签名原文。 */
926
+ interface PluginInstallConfirmationPackage {
927
+ packageId: string;
928
+ name: string;
929
+ version: string;
930
+ releaseStatus: 'released' | 'draft';
931
+ sizeBytes: number | null;
932
+ signatureState: 'signed' | 'unsigned';
933
+ installedVersion: string | null;
934
+ }
935
+ interface PluginInstallConfirmationRequest {
936
+ requestId: string;
937
+ pluginId: string;
938
+ workOrder?: string;
939
+ packages: PluginInstallConfirmationPackage[];
940
+ }
941
+ /**
942
+ * agentApi 2.2.0 的受控安装请求面。请求会立即返回逐包快照;可信确认、下载与
943
+ * 安装结果通过 onProgress 推送。进度事件与 request Promise 响应没有先后保证:
944
+ * 调用方必须先订阅,再按响应中的 requestId 和原始包集合缓冲、约束并合并事件。
945
+ */
946
+ interface PluginPlatformInstall {
947
+ getInstalledVersions(packageIds: string[]): Promise<Record<string, string | null>>;
948
+ request(input: PluginInstallRequestInput): Promise<PluginInstallRequestResult>;
949
+ onProgress(handler: (event: PluginInstallProgress) => void): () => void;
950
+ }
951
+ /**
952
+ * 全局结构化日志(Shell 内部 StationLogService;原 rti.global.logger 插件下沉)。
953
+ *
954
+ * **业务写入面(ADR-0002)**:插件应使用 `ctx.logger` 或 `@international-iot-association/plugin-logger` 门面。
955
+ * `ctx.platform.log` 仅保留兼容/查询;新业务代码不要再 `platform.log.write`。
956
+ * agentApi 3 的 ctx.platform 仍恒带 `log`(查询/stats/迁移期写入)。
957
+ * `source` 由宿主按 pluginId 盖章,调用方无法伪造。
958
+ */
959
+ type PlatformLogLevel = 'debug' | 'info' | 'warn' | 'error';
960
+ /** 插件 runtime 日志句柄(唯一业务写入面;可带结构化 data)。 */
961
+ interface PluginLogger {
962
+ debug(msg: string, data?: Record<string, unknown>): void;
963
+ info(msg: string, data?: Record<string, unknown>): void;
964
+ warn(msg: string, data?: Record<string, unknown>): void;
965
+ error(msg: string, data?: Record<string, unknown>): void;
966
+ }
967
+ interface PlatformLogEntryInput {
968
+ level: PlatformLogLevel;
969
+ message: string;
970
+ tags?: string[];
971
+ data?: Record<string, unknown>;
972
+ /** ISO 时间戳;缺省由 Shell 按接收时刻填充。 */
973
+ ts?: string;
974
+ }
975
+ interface PlatformLogEntry extends PlatformLogEntryInput {
976
+ ts: string;
977
+ /** 来源 pluginId(宿主填充)。 */
978
+ source: string;
979
+ }
980
+ interface PlatformLogWriteAck {
981
+ ok: boolean;
982
+ accepted: number;
983
+ error?: string;
984
+ }
985
+ interface PlatformLogQuery {
986
+ limit?: number;
987
+ levels?: PlatformLogLevel[];
988
+ sources?: string[];
989
+ tags?: string[];
990
+ }
991
+ interface PlatformLogQueryResult {
992
+ ok: boolean;
993
+ entries: PlatformLogEntry[];
994
+ error?: string;
995
+ }
996
+ interface PlatformLogStats {
997
+ ok: boolean;
998
+ currentFile: string;
999
+ writtenTotal: number;
1000
+ pending: number;
1001
+ files: string[];
1002
+ }
1003
+ interface PluginPlatformLog {
1004
+ write(entry: PlatformLogEntryInput): Promise<PlatformLogWriteAck>;
1005
+ writeBatch(entries: PlatformLogEntryInput[]): Promise<PlatformLogWriteAck>;
1006
+ query(q?: PlatformLogQuery): Promise<PlatformLogQueryResult>;
1007
+ stats(): Promise<PlatformLogStats>;
1008
+ }
1009
+ interface PluginPlatformRfStationAssetSnapshot {
1010
+ stationProfile: unknown;
1011
+ }
1012
+ interface PluginPlatformRfSecureMaterialRequest {
1013
+ kind: "string" | "auth" | "triple";
1014
+ ref: string;
1015
+ params?: Record<string, string | number | boolean>;
1016
+ }
1017
+ interface PluginPlatformRfSecureMaterialPreflightRequest {
1018
+ entries: Array<{
1019
+ kind: "string" | "auth" | "triple";
1020
+ ref: string;
1021
+ }>;
1022
+ }
1023
+ interface PluginPlatformRfSecureMaterialStringResponse {
1024
+ kind: "string";
1025
+ value: string;
1026
+ }
1027
+ interface PluginPlatformRfSecureMaterialAuthResponse {
1028
+ kind: "auth";
1029
+ cipher: string;
1030
+ source: "local-md5" | "certificate" | "simulated";
1031
+ }
1032
+ interface PluginPlatformRfSecureMaterialTripleResponse {
1033
+ kind: "triple";
1034
+ productId: string;
1035
+ deviceId: string;
1036
+ secret: string;
1037
+ source: "mes-lease" | "simulated";
1038
+ }
1039
+ type PluginPlatformRfSecureMaterialResponse = PluginPlatformRfSecureMaterialStringResponse | PluginPlatformRfSecureMaterialAuthResponse | PluginPlatformRfSecureMaterialTripleResponse;
1040
+ interface PluginPlatformRf {
1041
+ getStationAssetSnapshot(): Promise<PluginPlatformRfStationAssetSnapshot>;
1042
+ /** 只核对当前插件 scope 内的 ref 与类型,不读取或返回秘密。 */
1043
+ preflightSecureMaterials(request: PluginPlatformRfSecureMaterialPreflightRequest): Promise<{
1044
+ ok: true;
1045
+ }>;
1046
+ resolveSecureMaterial(request: PluginPlatformRfSecureMaterialRequest): Promise<PluginPlatformRfSecureMaterialResponse>;
1047
+ }
1048
+ /**
1049
+ * agentApi 3.0 的 ctx.platform:本地 run 生命周期 + 全局日志 + 可选 MES 代发。
1050
+ * 语义要点:
1051
+ * - begin/append/complete/interrupt 返回 durable receipt(本地 journal fsync);
1052
+ * complete/interrupt **不**触发 MES envelope 上报;
1053
+ * - plugin stamp 由外壳按 installed 登记盖章,绝不采信 runtime 自报身份;
1054
+ * - beginRun 默认仍是单 active run 语义;只有 `{ concurrent: true }` 才允许在
1055
+ * 同一 runtime 内并发开启第二个及以上 run;
1056
+ * - append/complete/interrupt 在单 active run 时可省略 runId 以兼容旧代码;
1057
+ * 若当前存在多个 active run,则省略 runId 会被宿主明确拒绝;
1058
+ * - `log` 恒注入(agentApi 3):NDJSON 由 Shell Main 落盘,无独立 logger 插件进程;
1059
+ * - `mes` 仅当 manifest 声明 `mes:api` 时注入;细权在机器 token 后端 scope;
1060
+ * - 无具名 invokeOperation 插件面(ADR-0001)。
1061
+ */
1062
+ interface PlatformBeginRunOptions {
1063
+ concurrent?: boolean;
1064
+ }
1065
+ interface PlatformRunTargetOptions {
1066
+ runId?: string;
1067
+ }
1068
+ /** 原生扫码结构化错误码(ADR-0009)。字符串错误只作诊断,业务必须看 code。 */
1069
+ type ScannerErrorCode = 'SCANNER_UNAVAILABLE' | 'CAMERA_NOT_FOUND' | 'CAMERA_OPEN_FAILED' | 'CAMERA_DISCONNECTED' | 'NO_CODE' | 'MULTIPLE_CODES' | 'TIMEOUT' | 'CANCELLED' | 'DUPLICATE_TRIGGER' | 'INVALID_REQUEST' | 'PERMISSION_DENIED';
1070
+ interface ScannerCameraInfo {
1071
+ cameraId: string;
1072
+ label: string;
1073
+ backend: 'mf' | 'avf' | 'fake';
1074
+ }
1075
+ interface ScannerRoi {
1076
+ x: number;
1077
+ y: number;
1078
+ width: number;
1079
+ height: number;
1080
+ }
1081
+ interface ScannerScanRequest {
1082
+ scanId: string;
1083
+ sessionId: string;
1084
+ seatIndex: number;
1085
+ cameraId: string;
1086
+ triggerSource: 'fixture-press' | 'manual-rescan';
1087
+ triggeredAt: string;
1088
+ deadlineMs: number;
1089
+ settleMs?: number;
1090
+ frameCount?: number;
1091
+ roi?: ScannerRoi;
1092
+ }
1093
+ interface ScannerCandidate {
1094
+ rawText: string;
1095
+ format: string;
1096
+ }
1097
+ interface ScannerSeatResult {
1098
+ cameraId: string;
1099
+ imageId: string;
1100
+ frameTs: string;
1101
+ ok: boolean;
1102
+ rawText?: string;
1103
+ candidates: ScannerCandidate[];
1104
+ captureMs: number;
1105
+ roiMs: number;
1106
+ decodeMs: number;
1107
+ width?: number;
1108
+ height?: number;
1109
+ roi?: ScannerRoi;
1110
+ errorCode?: ScannerErrorCode;
1111
+ errorMessage?: string;
1112
+ }
1113
+ interface NativeScanResult {
1114
+ scanId: string;
1115
+ sessionId: string;
1116
+ seatIndex: number;
1117
+ ok: boolean;
1118
+ result: ScannerSeatResult;
1119
+ totalMs: number;
1120
+ decoderVersion: string;
1121
+ cameraConfigVersion: string;
1122
+ }
1123
+ interface ScannerHealth {
1124
+ available: boolean;
1125
+ decoderVersion: string;
1126
+ cameras: Array<{
1127
+ cameraId: string;
1128
+ ready: boolean;
1129
+ errorCode?: ScannerErrorCode;
1130
+ }>;
1131
+ }
1132
+ /**
1133
+ * agentApi 3 + `platform:scanner`:按席触发式原生扫码(ADR-0009)。
1134
+ * 插件不得自己打开摄像头或解码。
1135
+ */
1136
+ interface PluginPlatformScanner {
1137
+ listCameras(): Promise<ScannerCameraInfo[]>;
1138
+ healthCheck(): Promise<ScannerHealth>;
1139
+ configure(cameras: Array<{
1140
+ cameraId: string;
1141
+ seatIndex: number;
1142
+ roi?: ScannerRoi;
1143
+ }>): Promise<{
1144
+ ok: true;
1145
+ }>;
1146
+ scan(request: ScannerScanRequest): Promise<NativeScanResult>;
1147
+ cancel(scanId: string): Promise<{
1148
+ ok: true;
1149
+ }>;
1150
+ }
1151
+ /**
1152
+ * 机台上传对象:把失败现场(过程日志、整窗截图)传到 MES 绑定的对象库。
1153
+ *
1154
+ * 插件只声明「传哪个 storage 相对路径」「要不要一张整窗截图」——STS 凭证、机器
1155
+ * token、bucket/endpoint 与文件字节全部收敛在 Shell 主进程,插件一概拿不到,
1156
+ * 通讯面上也只走路径字符串,不搬字节。
1157
+ *
1158
+ * **失败走返回值联合,不走 reject**:宿主把 rejection 压成 `err.message` 字符串
1159
+ * 转发,自定义字段会全部丢失,而产线 UI 规范要求故障代码必须能到操作员眼前。
1160
+ * 只有传输层问题(宿主未接线 / runtime 掉线)才会 reject。
1161
+ */
1162
+ type PluginUploadSource = {
1163
+ kind: 'storage-file';
1164
+ /** ctx.storage 相对路径,如 `runs/20260823T031500Z_s0_SN123_run-1.log`。 */
1165
+ path: string;
1166
+ /** 对象相对名的基名;缺省取 path 的 basename。宿主一律清洗,不采信原样。 */
1167
+ label?: string;
1168
+ contentType?: string;
1169
+ } | {
1170
+ kind: 'window-screenshot';
1171
+ /** 对象相对名的基名,缺省 `screen`。 */
1172
+ label?: string;
1173
+ } | {
1174
+ kind: 'dialog-file';
1175
+ /**
1176
+ * `ctx.dialog.openFile()` 返回的文件 handle(需 `ui:file-dialog` 权限)。
1177
+ *
1178
+ * 为什么是 handle 而不是路径:沙箱 UI 与插件 runtime 都拿不到用户选中文件的
1179
+ * 绝对路径(W1-7),宿主只发一个不透明 handle。上传时宿主用自己的登记表把它
1180
+ * 兑换回路径、自己读盘直传对象库——**字节与路径全程不进插件进程**,
1181
+ * 与 storage-file / window-screenshot 是同一条口径。
1182
+ *
1183
+ * 与设备通道兑换的差别:上传兑换**不消耗使用次数**,只受 handle 自身的
1184
+ * 有效期约束。理由是上传失败要能让操作员原地点「重试」,而不是把刚选的
1185
+ * 一二十个文件重选一遍;读文件本身是幂等的,与「同一份固件被重放进设备」
1186
+ * 不是一类风险。handle 过期或 runtime 停止后照常失效。
1187
+ */
1188
+ handle: string;
1189
+ /** 对象相对名的基名;缺省取宿主侧的文件名。宿主一律清洗,不采信原样。 */
1190
+ label?: string;
1191
+ contentType?: string;
1192
+ };
1193
+ interface PluginUploadRequest {
1194
+ /** 1..20 个来源,顺序即登记顺序。 */
1195
+ sources: PluginUploadSource[];
1196
+ /** 对象名分组段,如 `pcba/SN123`;宿主逐段清洗。缺省不分组。 */
1197
+ group?: string;
1198
+ /**
1199
+ * 归属测试记录,**必须成对**(宿主在任何网络调用之前就校验,单传其一直接
1200
+ * invalid-request)。`testKind` 只认 `'pcba'` / `'product'`,大小写敏感;
1201
+ * `testId` 必须是正整数。
1202
+ *
1203
+ * PCBA 站的取法:MES result-submit 的 live 2xx 回包带 `data.testRecordId`。
1204
+ * 回落 outbox(还没落库)、无写通道、只读模式都拿不到 id——那时两个字段**都**
1205
+ * 不要带,对象照样传得上去,只是在 Admin 测试详情页挂不出来,得去机台上传
1206
+ * 会话列表里翻。
1207
+ */
1208
+ testKind?: string;
1209
+ testId?: number;
1210
+ /** 本地 runId,仅进宿主日志用于串联,不上送后端。 */
1211
+ runId?: string;
1212
+ }
1213
+ interface PluginUploadedObject {
1214
+ objectId: string;
1215
+ relativeKey: string;
1216
+ sizeBytes: number;
1217
+ /** 对应 request.sources 的下标,便于插件回填自己的 UI。 */
1218
+ sourceIndex: number;
1219
+ }
1220
+ type PluginUploadFailureCode =
1221
+ /** token 缺 object-upload:write scope —— 找管理员开权限。 */
1222
+ 'permission-denied'
1223
+ /** 机台被后台锁定 —— 与缺 scope 同为 403,但要做的是解锁机台,不是开权限。 */
1224
+ | 'machine-locked'
1225
+ /** 机器令牌失效/被吊销(401)—— 要重新配对机台,重试无用。 */
1226
+ | 'unauthorized'
1227
+ /** 对象库拒绝(STS 票过期或策略不匹配)。重传会开新会话拿新票,故可重传。 */
1228
+ | 'store-denied' | 'invalid-request' | 'not-found' | 'conflict' | 'service-unavailable' | 'network' | 'source-missing' | 'source-too-large' | 'screenshot-unavailable'
1229
+ /** 机台本地上传通道暂时排满(并发或内存配额)—— 稍后重试即可。 */
1230
+ | 'busy' | 'internal';
1231
+ type PluginUploadOutcome = {
1232
+ ok: true;
1233
+ sessionId: string;
1234
+ objects: PluginUploadedObject[];
1235
+ totalBytes: number;
1236
+ durationMs: number;
1237
+ } | {
1238
+ ok: false;
1239
+ code: PluginUploadFailureCode;
1240
+ /** 稳定故障代码,直接进 UI「故障代码」栏,如 `UP-AUTH-403`。 */
1241
+ faultCode: string;
1242
+ /** 是否值得让操作员点「重新上传」。 */
1243
+ retryable: boolean;
1244
+ /** 技术细节(可能是后端英文原文),只进日志;禁止单独当操作员文案。 */
1245
+ detail: string;
1246
+ /** 后端 requestId(有则带),供跨系统追溯。 */
1247
+ requestId?: string;
1248
+ attempts: number;
1249
+ /** 失败前已成功传上去的对象数,用于「部分上传」提示。 */
1250
+ uploadedBeforeFailure: number;
1251
+ };
1252
+ interface PluginPlatformUpload {
1253
+ /**
1254
+ * 一次调用 = 一个上传会话,内含有限次退避重试;最终失败返回 `ok:false`,
1255
+ * **不落持久化队列**。将来若加队列,会新增 `{ ok:true, queued:true }` 分支,
1256
+ * 现有 `if (outcome.ok)` 判断不受影响。
1257
+ */
1258
+ submit(request: PluginUploadRequest): Promise<PluginUploadOutcome>;
1259
+ }
1260
+ /**
1261
+ * 一对已铸造的虚拟串口(socat PTY 对)。
1262
+ *
1263
+ * 两端是同一条管道的两头:往 `simulatorPath` 写的字节,从 `clientPath` 读得到,
1264
+ * 反之亦然。
1265
+ */
1266
+ interface VirtualSerialPairInfo {
1267
+ /** 释放时用的句柄。 */
1268
+ pairId: string;
1269
+ /**
1270
+ * **仿真器自己那一端。** 仿真插件用 `node:fs` / `serialport` 打开它,
1271
+ * 扮演被测设备读写字节。
1272
+ */
1273
+ simulatorPath: string;
1274
+ /**
1275
+ * **客户端那一端——只读文本,给人看的。**
1276
+ *
1277
+ * 把它显示出来供操作者复制,然后由**人**去工位插件的通道配置里填上。
1278
+ * Shell 不会、也没有能力替谁把它绑进任何通道(ADR-0031 D6):真机上没有谁会
1279
+ * 把串口自动绑进插件,仿真台跳过这一步,等于把「产线准备流程」这段最容易出错的
1280
+ * 部分从演练里删掉,到了真机上第一次做就是在产线上做。
1281
+ */
1282
+ clientPath: string;
1283
+ /** 铸造时传入的人类标签(如「夹具」「DUT」),未传为 null。 */
1284
+ label: string | null;
1285
+ /**
1286
+ * 铸造时传入的板级标识,未传为 null。
1287
+ *
1288
+ * 同一块**虚拟板**的多条 PTY 传同一个值——外壳会把它投影到
1289
+ * `SerialPortInfo.boardId`,让「哪两条口是同一块板」这件事有明确答案,
1290
+ * 而不是靠路径号相邻去猜(那正是 ADR-0031 D6 删掉的自动发现)。
1291
+ * 仿真台一席铸「夹具 + DUT」两条口时必须传同一个值,否则向导只能看到
1292
+ * 两条互不相干的口,配不成一套夹具。
1293
+ */
1294
+ boardId: string | null;
1295
+ createdAt: number;
1296
+ }
1297
+ /**
1298
+ * 虚拟串口能力(ADR-0031 D2)。可用需要**同时**满足两个条件,但两个条件
1299
+ * 各管一件事,**不要把它们混成一个判断**:
1300
+ *
1301
+ * 1. **manifest.permissions 含 `devtools:virtual-serial` → 决定这些方法在不在。**
1302
+ * 没声明权限时 `ctx.platform.virtualSerial` 为 `undefined`。
1303
+ * 2. **Shell 设置里的「虚拟串口」开关 → 决定调用成不成。**
1304
+ * 开关默认关闭;关着时方法**存在但调用抛错**,错误里带开启路径。
1305
+ *
1306
+ * 分工的原因是开关运行期可变(用户随时能关),而 ctx 只在 activate 时构一次;
1307
+ * 把开关冻进 ctx 会让「界面说关了、插件还以为开着」。所以:
1308
+ * **`undefined` 一律读作「这个插件没申请这项权限」,不是「开关没开」。**
1309
+ *
1310
+ * 两条路径都**绝不静默降级**——静默降级会让人以为仿真台在跑。调用方应当把
1311
+ * 抛出的原因原样显示给用户(含开启路径),而不是吞掉改走假数据。
1312
+ *
1313
+ * 仅 macOS 与 Linux 支持(依赖 socat 铸造 PTY 对),Windows 明确不支持。
1314
+ * **产线形态只验证过 macOS**——Linux 是为 CI 回归开放的,不要据此认为 Linux 工位可用。
1315
+ */
1316
+ interface PluginPlatformVirtualSerial {
1317
+ /**
1318
+ * 铸造一对 PTY。socat 缺失 / 平台不支持 / 开关关闭时抛错,**绝不静默成功**。
1319
+ *
1320
+ * `boardId`:同一块虚拟板的多条 PTY 传同一个值(见 `VirtualSerialPairInfo.boardId`)。
1321
+ */
1322
+ create(options?: {
1323
+ label?: string;
1324
+ boardId?: string;
1325
+ }): Promise<VirtualSerialPairInfo>;
1326
+ /** 释放一对 PTY(引用计数减一,归零时 Shell 杀掉 socat)。未知 pairId 幂等成功。 */
1327
+ release(pairId: string): Promise<{
1328
+ ok: true;
1329
+ }>;
1330
+ /** 本插件当前持有的 PTY 对。 */
1331
+ list(): Promise<VirtualSerialPairInfo[]>;
1332
+ }
1333
+ interface PluginPlatformApi {
1334
+ /** 当前平台运行模式(activate 下发初始值 + platformMode 推送即时更新)。 */
1335
+ readonly mode: PlatformRunMode;
1336
+ beginRun(subject: PlatformRunSubject, configRefs?: Record<string, string>, options?: PlatformBeginRunOptions): Promise<{
1337
+ receipt: PlatformDurableReceipt;
1338
+ mode: PlatformRunResultMode;
1339
+ }>;
1340
+ appendStep(step: PlatformRunStep, options?: PlatformRunTargetOptions): Promise<PlatformDurableReceipt>;
1341
+ completeRun(verdict: 'pass' | 'fail', options?: PlatformRunTargetOptions): Promise<PlatformDurableReceipt>;
1342
+ interruptRun(reason: string, options?: PlatformRunTargetOptions): Promise<PlatformDurableReceipt>;
1343
+ /** 全局结构化日志(Shell 常驻;source 由宿主盖章)。 */
1344
+ log: PluginPlatformLog;
1345
+ /**
1346
+ * MES path 客户端。仅 manifest.permissions 含 `mes:api` 时提供;
1347
+ * 否则为 undefined(与缺权限区分于「无凭据」——无凭据时 mes 仍在但调用抛错)。
1348
+ */
1349
+ mes?: PluginPlatformMes;
1350
+ /** manifest 声明 `platform:credential` 时提供。 */
1351
+ credential?: PluginPlatformCredential;
1352
+ /** manifest 声明 `platform:plugin-install` 时提供。 */
1353
+ pluginInstall?: PluginPlatformInstall;
1354
+ /**
1355
+ * 受保护 RF 工位资产与敏感材料能力。
1356
+ * 仅对声明 `platform:rf-station` 权限的 RF 插件提供。
1357
+ */
1358
+ rf?: PluginPlatformRf;
1359
+ /** manifest 声明 `platform:scanner` 时提供(ADR-0009)。 */
1360
+ scanner?: PluginPlatformScanner;
1361
+ /** manifest 声明 `platform:object-upload` 时提供(机台上传对象)。 */
1362
+ upload?: PluginPlatformUpload;
1363
+ /**
1364
+ * manifest 声明 `devtools:virtual-serial` 时提供(ADR-0031 D2/D3)。
1365
+ * **`undefined` = 没申请这项权限**;Shell 设置开关是否打开不影响这里在不在,
1366
+ * 它决定的是 `create()` 调用成不成(关着时抛错)。
1367
+ */
1368
+ virtualSerial?: PluginPlatformVirtualSerial;
1369
+ }
1370
+ /**
1371
+ * 在宿主进程内交给插件运行时的受控上下文
1372
+ *(构造于 electron-station-shell 的 plugin-host/runner.ts)。插件除此之外
1373
+ * 什么都拿不到——没有 fs、没有 db、没有真实串口、没有后端 token。
1374
+ */
1375
+ interface PluginRuntimeContext {
1376
+ /** 唯一业务日志写入面(ADR-0002);可选第二参为结构化 data。 */
1377
+ logger: PluginLogger;
1378
+ channels: {
1379
+ writeReadMock(logicalName: string, payload: string): Promise<{
1380
+ logicalName: string;
1381
+ request: string;
1382
+ response: string;
1383
+ latencyMs: number;
1384
+ }>;
1385
+ /**
1386
+ * 真实设备通道。路由路径为 runner -> host -> Agent Device Broker -> provider。
1387
+ * 插件永远看不到原始端口。需要在 manifest.permissions 中声明该通道。
1388
+ *
1389
+ * 电流表 qt-burst 连采:payload 为 `{ kind: "scpi-qt-burst", count, intervalMs, ... }`
1390
+ * 时,新外壳在 Agent 内循环 INITiate+READ?,一次返回 `acceptedSamplesA`。
1391
+ * 不是通用 TCP 剧本。老外壳会拒识该 payload,插件应回退逐枪 writeRead。
1392
+ */
1393
+ writeRead(logicalName: string, payload: unknown, options?: {
1394
+ timeoutMs?: number;
1395
+ codec?: 'json-lines' | 'modbus-rtu' | 'raw-text' | 'scpi-text';
1396
+ expectResponse?: boolean;
1397
+ /**
1398
+ * raw-text 专用:本次调用的「回包结束标记」。收到它之后只再等一小段
1399
+ * 静默就返回,不等满 timeoutMs。缺省 `CLI_OK`(工厂 CLI 的完成字样)。
1400
+ *
1401
+ * 为什么要按次传而不是写死通道级常量:回包里没有 `CLI_OK` 的命令
1402
+ * (不同固件/非 tct 指令)会一路等满超时,整条产测被拖长;调用方最清楚
1403
+ * 自己这条命令的结束标记是什么。匹配不区分大小写。
1404
+ */
1405
+ expectToken?: string;
1406
+ }): Promise<{
1407
+ logicalName: string;
1408
+ request: unknown;
1409
+ response: unknown;
1410
+ latencyMs: number;
1411
+ }>;
1412
+ /**
1413
+ * 订阅某个逻辑通道上的非请求触发(unsolicited)设备事件。需要在
1414
+ * manifest.permissions 中声明该通道。运行时停止时处理器会被丢弃。
1415
+ * serial 订阅本身不 open/独占物理串口;资源事件会扇出到 active logical consumer。
1416
+ * 返回反注册函数:移除本次注册的 handler(重复调用幂等)。宿主级订阅
1417
+ * 保持存在,由运行时停止统一回收(协议面无 unsubscribe 消息)。
1418
+ */
1419
+ subscribe(logicalName: string, handler: (event: Record<string, unknown>) => void): () => void;
1420
+ /**
1421
+ * 受控通道生命周期操作。插件仍然看不到真实串口句柄;宿主会按 manifest.permissions
1422
+ * 校验 logicalName。`configure/open/close/status/updateBaudRate` 必须声明对应通道的 `.control`
1423
+ * 权限,例如 `serial.dut.control` 或 `channel.serial.dut.control`;只声明
1424
+ * `serial.dut` / `serial.dut.writeRead` 只能读写/订阅,不能控制生命周期。
1425
+ * serial 的 open/close 是 caller-scoped attachment/detachment;同一 caller 必须
1426
+ * 先 open 才能 writeRead,最后一个 attachment 释放后才关闭物理串口。
1427
+ * `updateBaudRate` 只改已打开串口的波特率,不关口;非串口通道或不在 open 态会拒绝。
1428
+ * 未调用该动作的插件行为与以前完全相同。
1429
+ * `listPorts`/`detectDevices` 需要显式 `serial.discovery`(或粗粒度 `serial`)
1430
+ * 权限。`listPorts` 只返回 Agent 已合并过的端口元数据;`detectDevices` 返回
1431
+ * Agent 协议探测建议,但不授予任何额外通道访问权。
1432
+ */
1433
+ control(request: {
1434
+ action: 'listPorts';
1435
+ } | {
1436
+ action: 'detectDevices';
1437
+ } | {
1438
+ action: 'configure';
1439
+ config: DeviceChannelConfig;
1440
+ } | {
1441
+ action: 'open';
1442
+ logicalName: string;
1443
+ } | {
1444
+ action: 'close';
1445
+ logicalName: string;
1446
+ } | {
1447
+ action: 'status';
1448
+ logicalName: string;
1449
+ } | {
1450
+ action: 'updateBaudRate';
1451
+ logicalName: string;
1452
+ baudRate: number;
1453
+ }): Promise<SerialPortInfo[] | SerialDeviceSuggestion[] | ChannelStatus>;
1454
+ };
1455
+ /** 轴 B:向外壳上报进度/结果(日志 + 记录面板)。 */
1456
+ report: {
1457
+ stepEvent(event: {
1458
+ stepKey: string;
1459
+ phase?: string;
1460
+ message: string;
1461
+ }): void;
1462
+ result(result: StepResult): void;
1463
+ };
1464
+ /** 轴 A:通往插件 UI iframe 的传输层(由 @international-iot-association/plugin-sdk 使用)。 */
1465
+ ui: PluginUiTransport;
1466
+ /** 轴 D:插件间服务总线(由 @international-iot-association/plugin-sdk 的 service client/server 使用)。 */
1467
+ bus: PluginServiceBus;
1468
+ /** 插件私有本地存储;仅当 manifest.permissions 含 `"storage"` 时提供。 */
1469
+ storage?: PluginStorage;
1470
+ /**
1471
+ * 原生文件对话框(主进程执行);仅当 manifest.permissions 含 `"ui:file-dialog"`
1472
+ * 时提供,否则为 undefined。用于让操作员选择本机文件(如 OTA 固件)。宿主不返回
1473
+ * 绝对路径,只返回一次性 opaque handle(W1-7)——插件在设备通道 payload 里以
1474
+ * `fileHandle` 字段回传,宿主校验归属/有效期/次数后替换为真实路径读盘
1475
+ * (避免把大文件字节搬进沙箱,同时插件无法上传任意本地文件)。
1476
+ */
1477
+ dialog?: PluginDialog;
1478
+ /**
1479
+ * agentApi 2.0.0:平台 run 生命周期 + operation broker 通道。仅当
1480
+ * manifest.agentApi 主版本 >= 2 时注入;1.x 插件为 undefined(K3 adapter
1481
+ * 的 capability gate,见外壳 agent-api-adapters/)。`platform.credential`
1482
+ * 是 2.1.0 的独立可选能力,并额外受 `platform:credential` 权限约束。
1483
+ */
1484
+ platform?: PluginPlatformApi;
1485
+ }
1486
+ /** 原生「打开文件」对话框选项(子集,映射到 Electron dialog.showOpenDialog)。 */
1487
+ interface DialogOpenFileOptions {
1488
+ title?: string;
1489
+ buttonLabel?: string;
1490
+ /** 文件类型过滤,如 `[{ name: '固件', extensions: ['bin'] }]`。 */
1491
+ filters?: {
1492
+ name: string;
1493
+ extensions: string[];
1494
+ }[];
1495
+ /** 允许多选(默认单选)。 */
1496
+ multiSelections?: boolean;
1497
+ /**
1498
+ * 扩展名 allowlist(如 `[".bin"]`,大小写不敏感,可省略前导点)。
1499
+ * 宿主在对话框选择后强制校验,不匹配则整个请求以明确错误拒绝。
1500
+ */
1501
+ accept?: string[];
1502
+ /** 单文件大小上限(字节);任一选中文件超限则整个请求以明确错误拒绝。 */
1503
+ maxSizeBytes?: number;
1504
+ /**
1505
+ * 让宿主把选中文件的内容以 UTF-8 文本随结果直接回传(`contentUtf8`)。
1506
+ * 面向"读一份小配置/模板"的场景:插件不经 handle 兑换即可拿到内容,
1507
+ * 但仍拿不到绝对路径(W1-7 的口径不变)。
1508
+ * 必须同时给出 `maxSizeBytes` 且不得超过 `DIALOG_READ_CONTENT_MAX_BYTES`,
1509
+ * 否则整个请求以明确错误拒绝——内容走 IPC,尺寸必须有硬上限。
1510
+ * 与 `readContentBase64` 互斥。
1511
+ */
1512
+ readContent?: boolean;
1513
+ /**
1514
+ * 让宿主把选中文件的**原始字节**以 Base64 随结果回传(`contentBase64`)。
1515
+ * 面向 JPEG/PNG 等二进制:不能走 UTF-8。同样必须给 `maxSizeBytes` 且不超过硬顶。
1516
+ * 与 `readContent` 互斥。
1517
+ */
1518
+ readContentBase64?: boolean;
1519
+ }
1520
+ /** `readContent: true` 时单文件内容上限(字节)。超限拒绝而不是截断。 */
1521
+ declare const DIALOG_READ_CONTENT_MAX_BYTES: number;
1522
+ /** 用户选中的单个文件(W1-7:不暴露绝对路径,只给一次性 opaque handle)。 */
1523
+ interface DialogOpenFileEntry {
1524
+ /**
1525
+ * 一次性 opaque handle:由宿主登记(默认 10 分钟有效、可使用 1 次)。
1526
+ * 插件在通道 payload 的 `fileHandle` 字段回传,由宿主兑换为真实路径。
1527
+ */
1528
+ handle: string;
1529
+ /** 文件名(basename,不含目录)。 */
1530
+ name: string;
1531
+ /** 文件大小(字节)。 */
1532
+ sizeBytes: number;
1533
+ /** 仅 `readContent: true` 时存在:文件内容(UTF-8 文本)。 */
1534
+ contentUtf8?: string;
1535
+ /** 仅 `readContentBase64: true` 时存在:文件原始字节的 Base64。 */
1536
+ contentBase64?: string;
1537
+ }
1538
+ interface DialogOpenFileResult {
1539
+ canceled: boolean;
1540
+ /** 用户选择的文件(canceled 时为空数组)。 */
1541
+ files: DialogOpenFileEntry[];
1542
+ }
1543
+ /** 原生「另存为」对话框选项(映射到 Electron dialog.showSaveDialog)。 */
1544
+ interface DialogSaveFileOptions {
1545
+ title?: string;
1546
+ buttonLabel?: string;
1547
+ /** 文件类型过滤;Electron 会按选中的过滤器自动补扩展名。 */
1548
+ filters?: {
1549
+ name: string;
1550
+ extensions: string[];
1551
+ }[];
1552
+ /** 另存为默认文件名(不含目录)。 */
1553
+ defaultFileName?: string;
1554
+ /**
1555
+ * 要写入的内容(UTF-8 文本)。**由宿主代写**:插件把内容推给宿主,
1556
+ * 宿主在用户选定的位置写文件——插件全程拿不到绝对路径(与 openFile 同一口径)。
1557
+ * 上限 `DIALOG_SAVE_CONTENT_MAX_BYTES`,超限以明确错误拒绝。
1558
+ */
1559
+ contentUtf8: string;
1560
+ }
1561
+ /** saveFile 内容上限(字节)。超限拒绝而不是截断。 */
1562
+ declare const DIALOG_SAVE_CONTENT_MAX_BYTES: number;
1563
+ interface DialogSaveFileResult {
1564
+ canceled: boolean;
1565
+ /** 实际写入的文件名(basename;canceled 时不存在)。 */
1566
+ name?: string;
1567
+ /** 实际写入的字节数(canceled 时不存在)。 */
1568
+ sizeBytes?: number;
1569
+ }
1570
+ /** 原生文件对话框能力(需 `"ui:file-dialog"` 权限)。 */
1571
+ interface PluginDialog {
1572
+ /** 弹出「打开文件」对话框,返回一次性文件 handle(不返回绝对路径)。 */
1573
+ openFile(options?: DialogOpenFileOptions): Promise<DialogOpenFileResult>;
1574
+ /** 弹出「另存为」对话框并由宿主把 contentUtf8 写到用户选定的位置。 */
1575
+ saveFile(options: DialogSaveFileOptions): Promise<DialogSaveFileResult>;
1576
+ }
1577
+ /** 插件运行时的 activate() 允许返回的内容。 */
1578
+ interface ActivatedRuntime {
1579
+ deactivate?(): void | Promise<void>;
1580
+ }
1581
+ /** 插件运行时模块必须导出的内容。 */
1582
+ type PluginActivate = (ctx: PluginRuntimeContext) => ActivatedRuntime | void | Promise<ActivatedRuntime | void>;
1583
+ /** 由 preload 桥接层暴露在 window.stationPlugins 上的带类型 API。 */
1584
+ interface StationPluginsApi {
1585
+ listRemote(registryUrl: string): Promise<IpcResult<RemotePlugin[]>>;
1586
+ installPlugin(pluginId: string): Promise<IpcResult<InstalledPlugin>>;
1587
+ updatePlugin(pluginId: string): Promise<IpcResult<InstalledPlugin>>;
1588
+ listPluginVersions(pluginId: string): Promise<IpcResult<PluginVersionHistoryEntry[]>>;
1589
+ rollbackPlugin(pluginId: string, version: string): Promise<IpcResult<InstalledPlugin>>;
1590
+ cleanupPluginVersions(pluginId: string, keepLatest?: number): Promise<IpcResult<PluginVersionCleanupResult>>;
1591
+ /** W3-4:聚合版本状态查询(版本下拉的数据入口)。 */
1592
+ getVersionState(pluginId: string): Promise<IpcResult<PluginVersionState>>;
1593
+ /** 下载指定远端版本;已安装插件不会因此自动切为当前运行版本。 */
1594
+ downloadPluginVersion(pluginId: string, targetVersion: string): Promise<IpcResult<PluginVersionState>>;
1595
+ /** renderer 完成一键就绪画面切换后的可信回执。 */
1596
+ acknowledgeMachineReadyFocus(requestId: string): Promise<IpcResult<null>>;
1597
+ /**
1598
+ * W3-4:维护切换事务——目标版本必须已在本地;停止受影响 runtime、启动目标
1599
+ * 版本并等待 readiness;成功才原子提交 selectedVersion,失败恢复原版本。
1600
+ */
1601
+ switchVersion(pluginId: string, targetVersion: string): Promise<IpcResult<PluginVersionState>>;
1602
+ /** W3-4:明确维护确认——把当前运行版本记为 lastKnownGoodVersion。 */
1603
+ confirmVersionGood(pluginId: string): Promise<IpcResult<PluginVersionState>>;
1604
+ uninstallPlugin(pluginId: string): Promise<IpcResult<{
1605
+ pluginId: string;
1606
+ }>>;
1607
+ listInstalled(): Promise<IpcResult<InstalledPlugin[]>>;
1608
+ startRuntime(pluginId: string): Promise<IpcResult<RuntimeStatus>>;
1609
+ allowUnofficialPluginAndStart(prompt: UnofficialPluginRunPrompt): Promise<IpcResult<RuntimeStatus>>;
1610
+ stopRuntime(pluginId: string): Promise<IpcResult<RuntimeStatus>>;
1611
+ /** 轴 A:将来自 UI iframe 的不透明 api-bridge 信封转发给运行时。 */
1612
+ pluginMessage(pluginId: string, envelope: unknown): Promise<IpcResult<void>>;
1613
+ getPluginUiUrl(pluginId: string): Promise<IpcResult<string>>;
1614
+ getRuntimeLogs(pluginId: string): Promise<IpcResult<RuntimeLogEntry[]>>;
1615
+ getPaths(): Promise<IpcResult<StationPaths>>;
1616
+ /** 消费一次 Auth 请求的可信凭据配置入口;用于 renderer 重载后的可靠补投。 */
1617
+ consumeCredentialConfigurationRequest(): Promise<IpcResult<{
1618
+ requested: boolean;
1619
+ }>>;
1620
+ /** 消费待确认的插件安装请求;用于 renderer 重载后的可靠补投。 */
1621
+ consumePluginInstallConfirmationRequest(): Promise<IpcResult<PluginInstallConfirmationRequest | null>>;
1622
+ /** 对待确认请求作一次可信确认或拒绝;重复/未知 requestId fail-closed。 */
1623
+ respondPluginInstallRequest(requestId: string, confirmed: boolean): Promise<IpcResult<void>>;
1624
+ /**
1625
+ * 开发闭环(development profile 专用,production fail-closed):弹出目录选择框,
1626
+ * 从本地插件目录(构建产物:manifest.json + runtime-dist (+ ui-dist))安装为
1627
+ * local-dev 插件。用户取消返回 null。
1628
+ */
1629
+ selectAndInstallLocalPlugin(): Promise<IpcResult<{
1630
+ installed: InstalledPlugin;
1631
+ sourceDir: string;
1632
+ } | null>>;
1633
+ /**
1634
+ * 开发闭环:用本会话记录的来源目录重装该 local-dev 插件(运行中则先停后启)。
1635
+ * 缺省先在来源目录代跑插件自己的构建(`pnpm build`,可经
1636
+ * STATION_SHELL_LOCAL_BUILD_COMMAND 覆盖;输出进 runtime 日志),构建失败则
1637
+ * 不重装;`options.build: false` 跳过构建(已手动构建/构建环境异常时的逃生口)。
1638
+ */
1639
+ reloadLocalPlugin(pluginId: string, options?: {
1640
+ build?: boolean;
1641
+ }): Promise<IpcResult<InstalledPlugin>>;
1642
+ /** 订阅实时运行时事件;返回一个取消订阅的函数。 */
1643
+ onRuntimeEvent(cb: (event: RuntimeEvent) => void): () => void;
1644
+ }
1645
+ /**
1646
+ * 平台运行模式(W2-6,P0-04):
1647
+ * - `production`:正式生产。runner / Agent / 调试面板一律拒绝 Mock 通道。
1648
+ * - `maintenance-simulated`:受保护维护入口开启;所有 run 一律 simulated。
1649
+ * - `development`:开发 profile;所有 run 一律 simulated(不进正式 MES)。
1650
+ */
1651
+ type PlatformRunMode = 'production' | 'maintenance-simulated' | 'development';
1652
+ /** 机器凭据状态(脱敏;绝不含 token 明文或可恢复片段)。 */
1653
+ interface PlatformCredentialStatus {
1654
+ state: 'unconfigured' | 'configured' | 'reauth-required' | 'cipher-unavailable' | 'identity-mismatch';
1655
+ environmentTag?: string;
1656
+ verifiedAt?: string;
1657
+ /** 脱敏展示:仅 '••••' + 末 4 位。 */
1658
+ maskedToken?: string;
1659
+ }
1660
+ /** Shell 可信配置窗口中的 MES 服务选择;不会进入插件 iframe/runtime。 */
1661
+ type PlatformCredentialEndpointChoice = {
1662
+ kind: 'default';
1663
+ } | {
1664
+ kind: 'custom';
1665
+ baseOrigin: string;
1666
+ };
1667
+ /** token 与服务选择必须作为一次配置事务提交并共同验证。 */
1668
+ interface PlatformCredentialConfigureInput {
1669
+ token: string;
1670
+ endpoint: PlatformCredentialEndpointChoice;
1671
+ }
1672
+ /** 仅供 Shell renderer 的连接配置视图;默认服务地址本身永不返回。 */
1673
+ interface PlatformCredentialConfiguration {
1674
+ defaultAvailable: boolean;
1675
+ activeKind: 'none' | PlatformCredentialEndpointChoice['kind'];
1676
+ /** 当前 MES 服务 origin,仅供 Shell 可信设置/registry UI 展示。 */
1677
+ activeBaseOrigin?: string;
1678
+ /** 仅 activeKind=custom 时返回;插件状态投影不包含该字段。 */
1679
+ customBaseOrigin?: string;
1680
+ }
1681
+ /** 正式结果 outbox 概览。 */
1682
+ interface PlatformOutboxStats {
1683
+ pending: number;
1684
+ deadLetter: number;
1685
+ synced: number;
1686
+ oldestPendingAt: string | null;
1687
+ }
1688
+ interface PlatformFatalRecord {
1689
+ at: string;
1690
+ message: string;
1691
+ }
1692
+ /** 平台健康快照(P1-3;Agent 面板 / 诊断导出的数据面)。 */
1693
+ interface PlatformHealthSnapshot {
1694
+ at: string;
1695
+ /** runtime 重启次数(pluginId -> 次数;含 watchdog 自动重启)。 */
1696
+ runtimeRestarts: Record<string, number>;
1697
+ /** 每插件最近一次 fatal(fail-stop)。 */
1698
+ lastFatal: Record<string, PlatformFatalRecord>;
1699
+ agentRestarts: number;
1700
+ agentLastExit: PlatformFatalRecord | null;
1701
+ channelTimeouts: number;
1702
+ channelParseErrors: number;
1703
+ serialConflicts: number;
1704
+ outbox: PlatformOutboxStats | null;
1705
+ diskFreeBytes: number | null;
1706
+ activeDigests: Record<string, string>;
1707
+ diagnosticsDropped: number;
1708
+ }
1709
+ /**
1710
+ * 设备指纹身份(脱敏投影:只含最终指纹与来源构成布尔,绝不含任何原始
1711
+ * 硬件序列号/机器 GUID)。指纹由 Shell 主进程启动时解析:env 覆盖优先,
1712
+ * 否则自动派生 `v1:<sha256hex>`(OS 机器 ID + SMBIOS UUID + 首装随机 installId)。
1713
+ */
1714
+ interface PlatformDeviceIdentity {
1715
+ /** env 覆盖原文,或自动派生的 `v1:<sha256hex>`(67 字符)。 */
1716
+ fingerprint: string;
1717
+ source: 'env-override' | 'derived';
1718
+ /** 各分量是否参与了本次派生(env-override 时全 false)。 */
1719
+ components: {
1720
+ osMachineId: boolean;
1721
+ hardwareUuid: boolean;
1722
+ installId: boolean;
1723
+ };
1724
+ }
1725
+ /** MES 标准错误信封({error:{code,message,requestId}});requestId 用于对账报障。 */
1726
+ interface MesApiError {
1727
+ code: string;
1728
+ message: string;
1729
+ requestId?: string;
1730
+ }
1731
+ interface ManufacturingContextSku {
1732
+ skuId: string;
1733
+ skuName: string;
1734
+ /** 其余 *Params 等字段为 JSON 字符串或标量,Shell 原样透传不解析。 */
1735
+ [key: string]: unknown;
1736
+ }
1737
+ /** 机台制造上下文(GET /api/v1/manufacturing-context 的 data)。 */
1738
+ interface ManufacturingContext {
1739
+ machineId: string;
1740
+ assignmentId: string;
1741
+ /** 工位类型编码;展示字典见 Shell @shared/manufacturingContext(未知 → `工序 #n`)。 */
1742
+ stationType: number;
1743
+ workOrder: {
1744
+ workOrderId: string;
1745
+ } | null;
1746
+ sku: ManufacturingContextSku | null;
1747
+ }
1748
+ /** manufacturing-context 拉取结果(renderer 安全投影;不透传 token/URL)。 */
1749
+ type ManufacturingContextResult = {
1750
+ kind: 'ok';
1751
+ context: ManufacturingContext;
1752
+ fetchedAt: string;
1753
+ }
1754
+ /** HTTP 409:机台无生效工位分配——禁止开测语义,UI 呈警告态。 */
1755
+ | {
1756
+ kind: 'no-active-assignment';
1757
+ error: MesApiError | null;
1758
+ fetchedAt: string;
1759
+ }
1760
+ /** 机器凭据未配置/需重新验证:先配置凭据再拉取。 */
1761
+ | {
1762
+ kind: 'credential-unavailable';
1763
+ message: string;
1764
+ fetchedAt: string;
1765
+ } | {
1766
+ kind: 'error';
1767
+ /** null = 网络层失败(未取得 HTTP 状态)。 */
1768
+ httpStatus: number | null;
1769
+ error: MesApiError | null;
1770
+ message: string;
1771
+ fetchedAt: string;
1772
+ };
1773
+ /**
1774
+ * 屏幕物理校准记录(每工位/每显示器一份,落盘 display-calibration.json)。
1775
+ * 由 Shell 校准 Mask 产出:操作员用实体直尺在 10mm 校准线档位中选最接近的一档。
1776
+ */
1777
+ interface DisplayCalibration {
1778
+ /** 档位百分比(50–400),同时是插件 UI 的默认根字号基准。 */
1779
+ scalePercent: number;
1780
+ /** 派生冗余值:CSS px / 物理 mm = (96/25.4) × scalePercent/100;主进程按 scalePercent 重算。 */
1781
+ pxPerMm: number;
1782
+ /** ISO 8601 保存时间。 */
1783
+ updatedAt: string;
1784
+ }
1785
+ /**
1786
+ * Shell → 插件 iframe 的显示缩放消息:握手应答与校准变更广播共用。
1787
+ * 插件 SDK 的 useDisplayScale 监听该消息;与 type:'api' 的 API-Bridge 信封互不干扰。
1788
+ */
1789
+ interface ShellDisplayScaleMessage {
1790
+ source: 'station-shell';
1791
+ type: 'display-scale';
1792
+ scalePercent: number;
1793
+ }
1794
+ /** 插件 iframe → Shell 的显示缩放握手请求(SDK 挂载时发送,Shell 仅应答已注册 frame)。 */
1795
+ interface PluginUiDisplayScaleRequestMessage {
1796
+ source: 'station-plugin-ui';
1797
+ type: 'display-scale:request';
1798
+ }
1799
+ /**
1800
+ * 由 preload 桥接层暴露在 window.stationPlatform 上的平台 API(第四个具名 API)。
1801
+ * 安全边界:token 字符串只经 credentialConfigure 单向进入主进程,绝无任何
1802
+ * 回读通道;credentialStatus 只返回脱敏状态。
1803
+ */
1804
+ interface StationPlatformApi {
1805
+ platformGetMode(): Promise<IpcResult<PlatformRunMode>>;
1806
+ /**
1807
+ * 进入/退出 maintenance-simulated(二次确认由 UI 层执行)。
1808
+ * development profile 下拒绝(本就全 simulated,无维护开关)。
1809
+ */
1810
+ platformSetMaintenance(on: boolean): Promise<IpcResult<PlatformRunMode>>;
1811
+ credentialStatus(): Promise<IpcResult<PlatformCredentialStatus>>;
1812
+ /** 读取可信配置窗口所需的最小连接信息;不返回默认 URL。 */
1813
+ credentialConfiguration(): Promise<IpcResult<PlatformCredentialConfiguration>>;
1814
+ /** 配置/轮换机器凭据与服务选择。token 单向进入主进程,绝不回读。 */
1815
+ credentialConfigure(input: PlatformCredentialConfigureInput): Promise<IpcResult<void>>;
1816
+ outboxStats(): Promise<IpcResult<PlatformOutboxStats>>;
1817
+ /** 人工重试 dead-letter 中的一条结果。 */
1818
+ outboxRetry(runId: string): Promise<IpcResult<void>>;
1819
+ /** 导出 dead-letter 中的一条结果到 <userData>/exports/,返回导出路径。 */
1820
+ outboxExport(runId: string): Promise<IpcResult<string>>;
1821
+ healthSnapshot(): Promise<IpcResult<PlatformHealthSnapshot>>;
1822
+ /** 一键导出诊断包(spool + 平台审计)到 <userData>/exports/,返回 zip 路径。 */
1823
+ diagnosticExport(): Promise<IpcResult<string>>;
1824
+ /** 隔离调试区中的 simulated run 结果文件列表(绝不上传正式 MES)。 */
1825
+ debugResultsList(): Promise<IpcResult<string[]>>;
1826
+ /** 读取屏幕校准记录;null 表示未校准(视同 100%)。 */
1827
+ displayCalibrationGet(): Promise<IpcResult<DisplayCalibration | null>>;
1828
+ /** 保存校准档位(50–400);pxPerMm 由主进程重算,不信任渲染进程派生值。 */
1829
+ displayCalibrationSet(scalePercent: number): Promise<IpcResult<DisplayCalibration>>;
1830
+ /** 读取设备指纹身份(供管理员到后台 Admin → 工位管理 → 机台 登记设备指纹)。 */
1831
+ deviceIdentityGet(): Promise<IpcResult<PlatformDeviceIdentity>>;
1832
+ /** 拉取机台制造上下文(机台/工序/工单/型号);错误含 requestId 供报障对账。 */
1833
+ manufacturingContextGet(): Promise<IpcResult<ManufacturingContextResult>>;
1834
+ /**
1835
+ * MES 代发请求日志(Shell 侧栏;Main 内存环形缓冲,默认不落盘)。
1836
+ * 条目永不含 token / Authorization;插件 iframe 无此 API。
1837
+ */
1838
+ mesRequestLogList(): Promise<IpcResult<MesRequestLogListItemView[]>>;
1839
+ /** 单条 MES 请求详情(按需读取;大 body 不随列表轮询传输)。 */
1840
+ mesRequestLogGet(id: string): Promise<IpcResult<MesRequestLogDetailView | null>>;
1841
+ /** MES path outbox 队列快照(pending + dead-letter)。 */
1842
+ mesOutboxList(): Promise<IpcResult<MesOutboxListView>>;
1843
+ /** 机台配对展示态(无 session_secret / token)。 */
1844
+ pairingStatus(): Promise<IpcResult<MachinePairingStatusView>>;
1845
+ /** 安装包预置的 MES Base URL 列表。 */
1846
+ pairingPresets(): Promise<IpcResult<string[]>>;
1847
+ /**
1848
+ * Main 实际配对通道策略(与 MachinePairingOrchestrator 选项一致)。
1849
+ * Renderer 必须用此值做 HTTPS 预检,勿自行按 profile 另推一套。
1850
+ */
1851
+ pairingPolicy(): Promise<IpcResult<MachinePairingChannelPolicy>>;
1852
+ /** 开始/继续配对轮询(baseUrl 为 origin)。 */
1853
+ pairingStart(baseUrl: string): Promise<IpcResult<MachinePairingStatusView>>;
1854
+ pairingStop(): Promise<IpcResult<void>>;
1855
+ /**
1856
+ * 高级「重新配对」:清除本地凭据后进入配对。
1857
+ * UI 须先强警告。
1858
+ */
1859
+ pairingStartRepair(baseUrl: string): Promise<IpcResult<MachinePairingStatusView>>;
1860
+ }
1861
+ /** Main 下发的配对通道策略(默认预置是否含 development loopback HTTP)。 */
1862
+ interface MachinePairingChannelPolicy {
1863
+ allowInsecureLoopbackHttp: boolean;
1864
+ }
1865
+ /** Shell 可信配对页状态投影(永不含 session_secret / token)。 */
1866
+ interface MachinePairingStatusView {
1867
+ phase: 'idle' | 'selecting_endpoint' | 'waiting_approval' | 'claiming' | 'verifying' | 'configured' | 'rejected' | 'error';
1868
+ baseUrl: string | null;
1869
+ pairingCode: string | null;
1870
+ pairingCodeDisplay: string | null;
1871
+ qrPayload: string | null;
1872
+ codeExpiresAt: string | null;
1873
+ claimExpiresAt: string | null;
1874
+ rejectReason: string | null;
1875
+ message: string | null;
1876
+ hostname: string | null;
1877
+ installId: string | null;
1878
+ allowManualToken: boolean;
1879
+ }
1880
+ /** Shell 侧栏轮询的轻量 MES 请求条目;仅含列表渲染所需字段。 */
1881
+ interface MesRequestLogListItemView {
1882
+ id: string;
1883
+ at: string;
1884
+ method: MesHttpMethod;
1885
+ path: string;
1886
+ status: number | null;
1887
+ }
1888
+ /** Shell 按需读取的 MES 请求元数据(脱敏投影;与 Main 日志摘要字段对齐)。 */
1889
+ interface MesRequestLogEntryView extends MesRequestLogListItemView {
1890
+ pluginId: string;
1891
+ params?: Record<string, string>;
1892
+ durationMs: number | null;
1893
+ delivery: 'live' | 'outbox';
1894
+ runId?: string;
1895
+ receiptId?: string;
1896
+ requestBodySummary?: string | null;
1897
+ responseBodySummary?: string | null;
1898
+ error?: string | null;
1899
+ phase: 'live' | 'enqueue' | 'drain' | 'reject';
1900
+ }
1901
+ /** MES 请求详情(点击列表项后按需获取;仍为脱敏投影,不含 token / Authorization)。 */
1902
+ interface MesRequestLogDetailView extends MesRequestLogEntryView {
1903
+ requestBodyText?: string | null;
1904
+ responseBodyText?: string | null;
1905
+ requestBodyTruncated?: boolean;
1906
+ responseBodyTruncated?: boolean;
1907
+ }
1908
+ /** Shell 侧栏可见的 MES outbox 单条记录(不含 Authorization)。 */
1909
+ interface MesOutboxRecordView {
1910
+ receiptId: string;
1911
+ pluginId: string;
1912
+ runId: string;
1913
+ method: MesHttpMethod;
1914
+ path: string;
1915
+ params?: Record<string, string>;
1916
+ status: 'pending' | 'dead-letter' | 'done';
1917
+ attempts: number;
1918
+ maxAttempts: number;
1919
+ nextAttemptAt: string | null;
1920
+ lastError: string | null;
1921
+ lastHttpStatus: number | null;
1922
+ enqueuedAt: string;
1923
+ updatedAt: string;
1924
+ pausedForReauth?: boolean;
1925
+ }
1926
+ interface MesOutboxListView {
1927
+ pending: MesOutboxRecordView[];
1928
+ deadLetter: MesOutboxRecordView[];
1929
+ }
1930
+
1931
+ export { type ActivatedRuntime, CURRENT_METER_LOGICAL, type ChannelMetrics, type ChannelState, type ChannelStatus, type ChooseFixtureDutOptions, DIALOG_READ_CONTENT_MAX_BYTES, DIALOG_SAVE_CONTENT_MAX_BYTES, DUT_LOGICAL, type DeviceChannelConfig, type DialogOpenFileEntry, type DialogOpenFileOptions, type DialogOpenFileResult, type DialogSaveFileOptions, type DialogSaveFileResult, type DisplayCalibration, FIXTURE_BAUD_RATE, FIXTURE_BOARD_ADDRESS, FIXTURE_LOGICAL, type FixtureDutPair, type FixturePortPairSelection, type FixturePortSelection, type InstalledPlugin, type IpcResult, type JsonLinesCodecConfig, KNOWN_T9W_DUT_ENDPOINT, KNOWN_T9W_FIXTURE_ENDPOINT, type MachinePairingChannelPolicy, type MachinePairingStatusView, type ManufacturingContext, type ManufacturingContextResult, type ManufacturingContextSku, type MesApiError, type MesHttpMethod, type MesLiveResult, type MesOutboxListView, type MesOutboxRecordView, type MesParamValue, type MesQueuedResult, type MesRequestLogDetailView, type MesRequestLogEntryView, type MesRequestLogListItemView, type MesRequestOptions, type MesResult, type MockChannelConfig, type ModbusRtuCodecConfig, type MqttChannelConfig, type NativeScanResult, type OperatorSignals, type OssChannelConfig, type PlatformBeginRunOptions, type PlatformCredentialConfiguration, type PlatformCredentialConfigureInput, type PlatformCredentialEndpointChoice, type PlatformCredentialStatus, type PlatformDeviceIdentity, type PlatformDurableReceipt, type PlatformFatalRecord, type PlatformHealthSnapshot, type PlatformLogEntry, type PlatformLogEntryInput, type PlatformLogLevel, type PlatformLogQuery, type PlatformLogQueryResult, type PlatformLogStats, type PlatformLogWriteAck, type PlatformOutboxStats, type PlatformRunMode, type PlatformRunResultMode, type PlatformRunStep, type PlatformRunSubject, type PlatformRunTargetOptions, type PluginActivate, type PluginCredentialStatus, type PluginDialog, type PluginInstallConfirmationPackage, type PluginInstallConfirmationRequest, type PluginInstallPackageRequest, type PluginInstallProgress, type PluginInstallProgressStatus, type PluginInstallRequestInput, type PluginInstallRequestResult, type PluginKind, type PluginLogger, type PluginManifest, type PluginPlatformApi, type PluginPlatformCredential, type PluginPlatformHttp, type PluginPlatformInstall, type PluginPlatformLog, type PluginPlatformMes, type PluginPlatformRf, type PluginPlatformRfSecureMaterialAuthResponse, type PluginPlatformRfSecureMaterialPreflightRequest, type PluginPlatformRfSecureMaterialRequest, type PluginPlatformRfSecureMaterialResponse, type PluginPlatformRfSecureMaterialStringResponse, type PluginPlatformRfSecureMaterialTripleResponse, type PluginPlatformRfStationAssetSnapshot, type PluginPlatformScanner, type PluginPlatformUpload, type PluginPlatformVirtualSerial, type PluginRegistryDocument, type PluginRuntimeContext, type PluginServiceBus, type PluginSignatureState, type PluginStepDef, type PluginStorage, type PluginStorageDeclaration, type PluginStorageScope, type PluginUiDisplayScaleRequestMessage, type PluginUiTransport, type PluginUploadFailureCode, type PluginUploadOutcome, type PluginUploadRequest, type PluginUploadSource, type PluginUploadedObject, type PluginVersionCleanupResult, type PluginVersionHistoryEntry, type PluginVersionOption, type PluginVersionState, type RawTextCodecConfig, type RegistryPublisherKey, type RegistrySnapshot, type RemotePlugin, type RemotePluginVersionOption, type RuntimeEvent, type RuntimeLogEntry, type RuntimeState, type RuntimeStatus, type ScannerCameraInfo, type ScannerCandidate, type ScannerErrorCode, type ScannerHealth, type ScannerRoi, type ScannerScanRequest, type ScannerSeatResult, type ScpiTextCodecConfig, type SerialChannelConfig, type SerialCodecConfig, type SerialDeviceSuggestion, type SerialPortCapability, type SerialPortInfo, type SerialPortRole, type SerialProbeAttempt, type SerialProbeKind, type SerialUsageSnapshot, type SerialUsageState, type SerialUsageUser, type ShellDisplayScaleMessage, type StationPaths, type StationPlatformApi, type StationPluginsApi, type StepEvent, type StepResult, T9W_DUT_BAUD_RATE, type TcpChannelConfig, type UnofficialPluginRunPrompt, type VirtualSerialPairInfo, allSerialPortPaths, asChannelStatus, asResponseObject, asSerialDeviceSuggestions, asSerialPorts, buildDutRawTextSerialConfig, buildFixtureSerialConfig, chooseFixtureAndDutPortPairs, chooseFixtureAndDutPorts, isFixtureReadyEvent, looksLikeUsbSerialPort, parseOperatorSignals, preferredSerialPathFromPort, preferredSerialPortPath, serialEndpointId, serialPortHasEndpoint };