agentworkshop 0.9.0 → 0.9.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,69 @@
1
+ /**
2
+ * 试验计划安全校验(2026-11-02 上线前加固):
3
+ * 计划一旦被 HITL 批准,执行器会以平台身份直接写节点(单卡批准 = 全计划授权)。
4
+ * 因此**创建时与执行前都必须复核节点归属**,防止:
5
+ * - 跨线写:计划 lineId=A,levels 却点名 B 线的节点(越权写别的产线);
6
+ * - 类型错配:targets 写成数控节点(读不到真值)或控制量其实是数采节点;
7
+ * - 量程外水平:创建时就拒绝,避免批准后逐步被 write() 打回、白花产线时间。
8
+ * 纯校验函数,直接读控制器/节点仓(无副作用,不写库)。
9
+ */
10
+ import { AppError } from '../../../utils/errors'
11
+ import { getDcwController } from '../dcw/dcw-controller'
12
+ import { getDaqNodeRepo } from '../daq/daq-node.repo'
13
+ import type { ExperimentPlanBody } from './experiment-runner'
14
+
15
+ export interface PlanNodeIssue {
16
+ kind: 'control' | 'target' | 'level'
17
+ nodeId: string
18
+ message: string
19
+ }
20
+
21
+ /**
22
+ * 校验计划节点集合与量程。返回问题列表(空 = 通过);
23
+ * 调用方决定抛错(创建/执行前)还是仅告警。
24
+ */
25
+ export function validatePlanNodes(plan: Pick<ExperimentPlanBody, 'lineId' | 'levels' | 'targets'>): PlanNodeIssue[] {
26
+ const issues: PlanNodeIssue[] = []
27
+ const controller = getDcwController()
28
+ for (const [controlId, levels] of Object.entries(plan.levels)) {
29
+ const node = controller.byId(controlId)
30
+ if (!node) {
31
+ issues.push({ kind: 'control', nodeId: controlId, message: `控制节点 ${controlId} 不存在(须为 DCW 数控节点)` })
32
+ continue
33
+ }
34
+ if (node.lineId && node.lineId !== plan.lineId) {
35
+ issues.push({ kind: 'control', nodeId: controlId, message: `控制节点 ${controlId} 属于产线 ${node.lineId},与计划产线 ${plan.lineId} 不一致(禁止跨线试验)` })
36
+ }
37
+ if (!node.enabled) {
38
+ issues.push({ kind: 'control', nodeId: controlId, message: `控制节点 ${controlId} 已停用(控制已暂停),不能用于试验` })
39
+ }
40
+ for (const level of levels) {
41
+ if (!Number.isFinite(level)) continue
42
+ if (level < node.min || level > node.max) {
43
+ issues.push({ kind: 'level', nodeId: controlId, message: `水平 ${level} 超出节点安全量程 ${node.min}~${node.max}${node.unit ?? ''}` })
44
+ }
45
+ }
46
+ }
47
+ for (const [controlId, targetId] of Object.entries(plan.targets ?? {})) {
48
+ if (!plan.levels[controlId]) {
49
+ issues.push({ kind: 'target', nodeId: targetId, message: `观测目标绑定到了未声明的控制量 ${controlId}` })
50
+ continue
51
+ }
52
+ const daq = getDaqNodeRepo().byId(targetId)
53
+ if (!daq) {
54
+ issues.push({ kind: 'target', nodeId: targetId, message: `观测目标 ${targetId} 不是数采节点(判读需要 DAQ 序列)` })
55
+ continue
56
+ }
57
+ if (daq.lineId && daq.lineId !== plan.lineId) {
58
+ issues.push({ kind: 'target', nodeId: targetId, message: `观测目标 ${targetId} 属于产线 ${daq.lineId},与计划产线 ${plan.lineId} 不一致` })
59
+ }
60
+ }
61
+ return issues
62
+ }
63
+
64
+ /** 创建/执行前强校验:有问题即 422(消息合并,便于一次改全) */
65
+ export function assertPlanNodesSafe(plan: Pick<ExperimentPlanBody, 'lineId' | 'levels' | 'targets'>): void {
66
+ const issues = validatePlanNodes(plan)
67
+ if (issues.length === 0) return
68
+ throw new AppError(422, 'EXPERIMENT_PLAN_NODE_INVALID', `试验计划节点校验失败:${issues.map(i => i.message).join(';')}`)
69
+ }
@@ -1 +0,0 @@
1
- {"id":"f507c33b-4ecc-4e32-af88-2e3736b443e9","timestamp":1793625297927,"prerendered":[]}