agentworkshop 0.8.5 → 0.8.6
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.
- package/.AgentWorkShop/prompts/host-tools.en.json +3 -3
- package/.AgentWorkShop/prompts/host-tools.json +14 -4
- package/.output/.AgentWorkShop/prompts/host-tools.en.json +3 -3
- package/.output/.AgentWorkShop/prompts/host-tools.json +14 -4
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{U_n8pryv2.js → 7op-BMk82.js} +1 -1
- package/.output/public/_nuxt/{zWZ26UWi.js → B1SIgyrl.js} +1 -1
- package/.output/public/_nuxt/{C4tXwpSt.js → BA2lmfW4.js} +1 -1
- package/.output/public/_nuxt/{EJkmXP432.js → BDXQqSkl2.js} +1 -1
- package/.output/public/_nuxt/{CY_7u1ST2.js → BI9jj13f2.js} +1 -1
- package/.output/public/_nuxt/{CGf9neEK2.js → BiXqyYNw2.js} +1 -1
- package/.output/public/_nuxt/{D9GoOkyP2.js → BlOm8JIs2.js} +1 -1
- package/.output/public/_nuxt/{CQ31GiJU2.js → BoXKUOiN2.js} +1 -1
- package/.output/public/_nuxt/{DZzbCFY2.js → BuLoPmcf.js} +1 -1
- package/.output/public/_nuxt/{D9ppMCbw.js → BwOK42JQ.js} +1 -1
- package/.output/public/_nuxt/{CZJ5cw6r2.js → C1RMfPoB2.js} +1 -1
- package/.output/public/_nuxt/{CGviZq4E.js → COfo_Bg3.js} +1 -1
- package/.output/public/_nuxt/CTGJHZ8X2.js +1 -0
- package/.output/public/_nuxt/{SgsmmrsZ.js → CY2NkFvI.js} +1 -1
- package/.output/public/_nuxt/{B4GOTVfN.js → C_4mzQ5X.js} +1 -1
- package/.output/public/_nuxt/{DGmFHb7m.js → CdvMP3lv.js} +1 -1
- package/.output/public/_nuxt/{iqxZGXsY.js → CooauJ7D.js} +1 -1
- package/.output/public/_nuxt/{C3vxePED.js → D1eVi1zr.js} +1 -1
- package/.output/public/_nuxt/{DnuJ66Jy.js → D6PUmBb0.js} +1 -1
- package/.output/public/_nuxt/{BBhroEeN.js → DC8sMA36.js} +1 -1
- package/.output/public/_nuxt/{DOUuGOQR2.js → DDHh85c42.js} +1 -1
- package/.output/public/_nuxt/{Cy_HDgSl.js → DDXOfmo2.js} +2 -2
- package/.output/public/_nuxt/{Cr2xJ_CY.js → DMFwLxfu.js} +1 -1
- package/.output/public/_nuxt/{BZg_MIK42.js → DoprKJ5a2.js} +1 -1
- package/.output/public/_nuxt/{BzBBEE-o2.js → Dr1FVeQG2.js} +1 -1
- package/.output/public/_nuxt/{W0sNnJQh.js → DtnsTW7e.js} +1 -1
- package/.output/public/_nuxt/{gUYi1hLJ2.js → DxDqd6T42.js} +1 -1
- package/.output/public/_nuxt/{B0UUODeh.js → DzU3zEKX.js} +1 -1
- package/.output/public/_nuxt/{BkO5WdX5.js → EgO70wrE.js} +1 -1
- package/.output/public/_nuxt/{7OZQiw002.js → HJKu4eEV2.js} +1 -1
- package/.output/public/_nuxt/{ChF6qLpY2.js → K5Xen0wZ2.js} +1 -1
- package/.output/public/_nuxt/{CNrRZmSx2.js → Rb4o4o-82.js} +1 -1
- package/.output/public/_nuxt/{BqBTlfiz.js → Tf6SJSDj.js} +1 -1
- package/.output/public/_nuxt/{DJaGVY4v2.js → ZzOyoXex2.js} +1 -1
- package/.output/public/_nuxt/{PbzVdCTA2.js → azOGy_tU2.js} +1 -1
- package/.output/public/_nuxt/{D1thFC3z.js → be-4wyTo.js} +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/593b65bd-cf1d-4b7f-a993-57e939c38f36.json +1 -0
- package/.output/public/_nuxt/{8b9CKi6F.js → dcjPjLPI.js} +1 -1
- package/.output/public/_nuxt/{ae2-5ZPt.js → h-Fn7pRD.js} +1 -1
- package/.output/public/_nuxt/{-gy9k_TE2.js → hyLdZsOd2.js} +1 -1
- package/.output/public/_nuxt/{BHxG41IQ2.js → o8sYG6xx2.js} +1 -1
- package/.output/server/chunks/_/access.mjs +1 -1
- package/.output/server/chunks/_/agent-dto.mjs +26 -0
- package/.output/server/chunks/_/agent-dto.mjs.map +1 -0
- package/.output/server/chunks/_/data-export.mjs +6 -6
- package/.output/server/chunks/_/data-query.mjs +3 -3
- package/.output/server/chunks/_/eda-run.repo.mjs +2 -2
- package/.output/server/chunks/_/features.mjs +5 -5
- package/.output/server/chunks/_/golden.mjs +3 -3
- package/.output/server/chunks/_/harness-spawn-policy.mjs +77 -0
- package/.output/server/chunks/_/harness-spawn-policy.mjs.map +1 -0
- package/.output/server/chunks/_/index.mjs +1 -1
- package/.output/server/chunks/_/line-scope.mjs +32 -0
- package/.output/server/chunks/_/line-scope.mjs.map +1 -0
- package/.output/server/chunks/_/mes-console.mjs +1 -1
- package/.output/server/chunks/_/nitro.mjs +2429 -1726
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/_/ocap.mjs +1 -1
- package/.output/server/chunks/_/rest-auth.mjs +1 -1
- package/.output/server/chunks/_/spc.mjs +1 -1
- package/.output/server/chunks/build/TownView-BWMDbosY.mjs +1 -1
- package/.output/server/chunks/build/daq-CdxcfY_G.mjs +1 -1
- package/.output/server/chunks/build/dcw-BlDQtS4q.mjs +1 -1
- package/.output/server/chunks/build/types-D6J7slEa.mjs +1 -1
- package/.output/server/chunks/build/useDaqAddNode-D_Rkju0f.mjs +1 -1
- package/.output/server/chunks/build/useDaqDetailNode-DeOsDxK5.mjs +1 -1
- package/.output/server/chunks/build/useDaqDetailParams-Cm9a9r5G.mjs +1 -1
- package/.output/server/chunks/build/useDaqFilters-CctmO03p.mjs +1 -1
- package/.output/server/chunks/build/useDaqStream-B9ZBVQrI.mjs +1 -1
- package/.output/server/chunks/build/useDaqTemplates-CK37sMfi.mjs +1 -1
- package/.output/server/chunks/build/useDcwAddNode-DACVcEHs.mjs +1 -1
- package/.output/server/chunks/build/useDcwStream-Dir5ufAU.mjs +1 -1
- package/.output/server/chunks/routes/api/mcp/workshop.mjs +1 -1
- package/.output/server/chunks/routes/api/system/backup/run.post.mjs +2 -2
- package/.output/server/chunks/routes/api/workshop/agent-tools/index.post.mjs +17 -9
- package/.output/server/chunks/routes/api/workshop/agent-tools/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/agents/_id_.patch.mjs +3 -0
- package/.output/server/chunks/routes/api/workshop/agents/_id_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id_.patch.mjs +6 -0
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/agents/_agentId_.patch.mjs +5 -0
- package/.output/server/chunks/routes/api/workshop/channels/_id/agents/_agentId_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index.get.mjs +5 -2
- package/.output/server/chunks/routes/api/workshop/channels/_id/index.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index.post.mjs +7 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/index.get.mjs +3 -1
- package/.output/server/chunks/routes/api/workshop/channels/index.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs +8 -6
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/ingest.post.mjs +29 -49
- package/.output/server/chunks/routes/api/workshop/daq/_id/ingest.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/samples.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id_.patch.mjs +6 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/alarms.get.mjs +4 -2
- package/.output/server/chunks/routes/api/workshop/daq/alarms.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/index2.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/infra/reconnect.post.mjs +14 -5
- package/.output/server/chunks/routes/api/workshop/daq/infra/reconnect.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/infra.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/test-driver.post.mjs +7 -4
- package/.output/server/chunks/routes/api/workshop/daq/test-driver.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/param-ledger.get.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/_id/param-ledger.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/read.post.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/_id/read.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/test.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/write.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.patch.mjs +6 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/controller.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index.get.mjs +7 -4
- package/.output/server/chunks/routes/api/workshop/dcw/index.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index2.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index2.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index3.get.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/index3.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index3.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs +7 -4
- package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index4.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index5.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index6.get.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/index6.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/journal/node/_nodeId/rollback.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/line/query.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/mes-artifacts/file.get.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/mes-artifacts/file.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/mes-hook-test.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/mes-test-read.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/judge.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/rollback.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/series.get.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/series.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id_.get.mjs +4 -2
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id_.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/read.post.mjs +5 -2
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/read.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/write.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/apply.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/rollback-good.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs +7 -3
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.delete.mjs +8 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/close.post.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/close.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/data.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/rollback.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/test-driver.post.mjs +7 -4
- package/.output/server/chunks/routes/api/workshop/dcw/test-driver.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/eda/commonality.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/eda/vm/predict.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/exp/confirmations/_id/decide.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/exp/index.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/fs/dirs.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index.post.mjs +2 -0
- package/.output/server/chunks/routes/api/workshop/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index3.post.mjs +8 -3
- package/.output/server/chunks/routes/api/workshop/index3.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/index5.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index6.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index7.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index7.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/mes/datasets.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/autofix.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/compile.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/evidence.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/publish.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/approval-decision.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/approval.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/transition.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/subgraph.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/validate.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId_.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/export/_ontologyId_.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/index.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/scene.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/traces/_ontologyId_.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/versions/_ontologyId/compare.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/ontology/versions/_ontologyId_.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/capabilities.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/index.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/index.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/index2.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/profiles/_id/publish.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/recommendations/_id/crosscheck.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/recommendations/_id/dispatch.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/optimization/solve.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/process-model/datasets.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/process-model/eligibility.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/process-model/index.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/process-model/specs/_id/publish.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/process-model/train.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/tools/sync.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs +2 -2
- package/.output/server/chunks/virtual/entry.mjs +1 -1
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README-zh.md +4 -2
- package/README.md +9 -2
- package/config.yml +1 -1
- package/docker-compose.yml +4 -2
- package/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/plugins.en.md +1 -1
- package/docs/plugins.md +1 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/package.json +1 -1
- package/scripts/_audit/_live-mqtt-e2e-20261028.mjs +219 -0
- package/scripts/_audit/prompt-injection-probe.mts +76 -0
- package/scripts/api-live-e2e.mjs +9 -3
- package/scripts/e2e-multiexpert-remediation-real.mjs +453 -0
- package/scripts/e2e-node-binding-3mode-real.mjs +697 -0
- package/scripts/e2e-node-binding-authz.mjs +332 -0
- package/scripts/security-acceptance.mjs +218 -0
- package/server/api/workshop/agent-tools/bindings/index.post.ts +33 -17
- package/server/api/workshop/agents/[id].patch.ts +3 -0
- package/server/api/workshop/agents/index.post.ts +3 -0
- package/server/api/workshop/channel-templates/[id].patch.ts +5 -0
- package/server/api/workshop/channel-templates/index.post.ts +6 -0
- package/server/api/workshop/channels/[id]/agents/[agentId].patch.ts +5 -0
- package/server/api/workshop/channels/[id]/agents/index.get.ts +8 -2
- package/server/api/workshop/channels/[id]/agents/index.post.ts +7 -0
- package/server/api/workshop/channels/[id]/index.get.ts +10 -4
- package/server/api/workshop/channels.post.ts +3 -0
- package/server/api/workshop/daq/[id]/ingest.post.ts +26 -22
- package/server/api/workshop/daq/[id].patch.ts +8 -0
- package/server/api/workshop/daq/alarms.get.ts +18 -16
- package/server/api/workshop/daq/infra/reconnect.post.ts +21 -5
- package/server/api/workshop/daq/test-driver.post.ts +10 -3
- package/server/api/workshop/dcw/[id]/param-ledger.get.ts +7 -1
- package/server/api/workshop/dcw/[id]/read.post.ts +6 -1
- package/server/api/workshop/dcw/[id].patch.ts +49 -40
- package/server/api/workshop/dcw/journal/index.get.ts +7 -3
- package/server/api/workshop/dcw/mes-artifacts/file.get.ts +7 -1
- package/server/api/workshop/dcw/mes-artifacts/index.get.ts +7 -1
- package/server/api/workshop/dcw/optimizations/[id]/series.get.ts +6 -1
- package/server/api/workshop/dcw/optimizations/[id].get.ts +3 -1
- package/server/api/workshop/dcw/optimizations/index.get.ts +6 -3
- package/server/api/workshop/dcw/params/[id]/read.post.ts +22 -17
- package/server/api/workshop/dcw/recipes/[id]/versions.get.ts +20 -14
- package/server/api/workshop/dcw/recipes/[id].delete.ts +33 -25
- package/server/api/workshop/dcw/recipes/index.get.ts +17 -12
- package/server/api/workshop/dcw/runs/[id]/close.post.ts +21 -15
- package/server/api/workshop/dcw/test-driver.post.ts +21 -16
- package/server/api/workshop/ws/hub.ts +2 -1
- package/server/plugins/01-instance-guard.ts +82 -0
- package/server/plugins/daq.ts +40 -8
- package/server/plugins/workshop.ts +12 -0
- package/server/services/workshop/agents/adapters/line-spawn.ts +24 -1
- package/server/services/workshop/agents/agent-dto.ts +54 -0
- package/server/services/workshop/agents/harness-spawn-policy.ts +114 -0
- package/server/services/workshop/agents/host-tool-bridge/dispatch.ts +22 -0
- package/server/services/workshop/agents/host-tool-bridge/index.ts +1 -1
- package/server/services/workshop/agents/host-tool-bridge/per-agent.ts +17 -3
- package/server/services/workshop/agents/host-tool-bridge/tools/tasks.ts +1 -1
- package/server/services/workshop/agents/host-tool-bridge/tools/team-delegation.ts +5 -1
- package/server/services/workshop/agents/industrial/dcw-tools.ts +6 -2
- package/server/services/workshop/agents/industrial/ops-tools.ts +54 -12
- package/server/services/workshop/agents/industrial/recipe-gate.ts +68 -15
- package/server/services/workshop/agents/industrial/tool-verify-legacy-specs.ts +2 -2
- package/server/services/workshop/agents/industrial/tool-verify.ts +31 -2
- package/server/services/workshop/agents/node-bindings.repo.ts +4 -3
- package/server/services/workshop/agents/node-delegation.ts +49 -4
- package/server/services/workshop/agents/tool-approvals.ts +23 -0
- package/server/services/workshop/agents/tool-reference-spec.ts +3 -0
- package/server/services/workshop/daq/bus/mqtt.adapter.ts +12 -0
- package/server/services/workshop/daq/daq-controller/crud.ts +5 -3
- package/server/services/workshop/daq/daq-controller/ingest.ts +23 -0
- package/server/services/workshop/daq/daq-controller/push.ts +24 -3
- package/server/services/workshop/daq/daq-controller/runtime-control.ts +6 -0
- package/server/services/workshop/daq/daq-controller/sweep-alarms.ts +3 -1
- package/server/services/workshop/daq/daq-controller/tsdb-twin.ts +21 -1
- package/server/services/workshop/daq/daq-node.repo.ts +10 -7
- package/server/services/workshop/daq/daq-node.ts +2 -0
- package/server/services/workshop/daq/daq-runtime.ts +10 -2
- package/server/services/workshop/daq/drivers/modbus-tcp.ts +8 -5
- package/server/services/workshop/daq/drivers/registry.ts +5 -4
- package/server/services/workshop/daq/infra.ts +13 -0
- package/server/services/workshop/daq/ingest-policy.ts +24 -1
- package/server/services/workshop/daq/storage/timescale.adapter.ts +42 -3
- package/server/services/workshop/db/database/migrations.ts +83 -68
- package/server/services/workshop/dcw/dcw-controller/crud.ts +27 -2
- package/server/services/workshop/dcw/dcw-controller/lines.ts +14 -3
- package/server/services/workshop/dcw/dcw-controller/recipes.ts +63 -5
- package/server/services/workshop/dcw/dcw-recipe.repo.ts +10 -1
- package/server/services/workshop/dcw/dcw-runtime.ts +37 -14
- package/server/services/workshop/dcw/drivers/shared.ts +62 -6
- package/server/services/workshop/dcw/recipe-op-anchor.ts +28 -0
- package/server/services/workshop/dcw/recipe-rollback-manager/rollback.ts +204 -190
- package/server/services/workshop/dcw/recipe-rollback-manager/sweep.ts +97 -77
- package/server/services/workshop/line-scope.ts +80 -0
- package/server/services/workshop/optimization/dispatch.service.ts +18 -2
- package/server/services/workshop/permissions.ts +2 -0
- package/server/services/workshop/runtime/agent-runtime/message.ts +17 -0
- package/server/services/workshop/runtime/agent-runtime/state.ts +9 -3
- package/server/services/workshop/runtime/agent-runtime/types.ts +2 -1
- package/server/services/workshop/runtime/channel-runtime.ts +3 -1
- package/server/services/workshop/runtime/mailbox.ts +6 -3
- package/server/services/workshop/runtime/manager/a2a.ts +4 -2
- package/server/services/workshop/runtime/manager/admin-channel.ts +1 -1
- package/server/services/workshop/runtime/manager/chat.ts +3 -1
- package/server/services/workshop/runtime/manager/host-tools.ts +1 -1
- package/server/services/workshop/runtime/manager/runtime-wiring.ts +31 -19
- package/server/services/workshop/runtime/manager/state.ts +43 -1
- package/server/services/workshop/runtime/permission-scope.ts +21 -0
- package/server/services/workshop/runtime/scheduler-loop/tick.ts +16 -4
- package/server/services/workshop/scene-events.ts +19 -0
- package/shared/daq-protocol.ts +24 -5
- package/shared/dcw-protocol/catalog.ts +4 -4
- package/.output/public/_nuxt/CCI-fwcS2.js +0 -1
- package/.output/public/_nuxt/builds/meta/5cd11284-05bf-4fcd-9a69-56a01c9f56dd.json +0 -1
|
@@ -235,80 +235,95 @@ export function migrateMissingForeignKeys(db: DatabaseSync): void {
|
|
|
235
235
|
* 身份与 token 已迁移至全局用户系统(data/users.sqlite),owner 列仅存用户 id 引用,
|
|
236
236
|
* 保留 FK 会使新全局用户 id 在本地 users 表无行而写入失败。
|
|
237
237
|
* 表重建期间临时关闭外键,避免 DROP 触发级联;重建后以原名重新命名,引用方 FK 依旧有效。
|
|
238
|
+
*
|
|
239
|
+
* 2026-10-28 审查 D-P1-2:旧实现用**白名单 DDL** 重建 channels/agents/teams ——
|
|
240
|
+
* 白名单随后续版本加列而漂移(channels 缺 scenario_prompt/llm_json/visibility/
|
|
241
|
+
* join_policy/approval_policy/chat_enabled/version/line_id 共 8 列),带 owner FK 的
|
|
242
|
+
* 老库升级时整列数据被静默丢弃且不可复现。现在每条重建语句都携带**完整列清单**,
|
|
243
|
+
* 由 tests/migration-owner-fk.test.ts 守护:升级前后"列集 + 全部业务数据"逐列相等
|
|
244
|
+
* (任一列漏抄 → 列集变化 → 测试红)。
|
|
238
245
|
*/
|
|
239
246
|
export function migrateDropOwnerFks(db: DatabaseSync): void {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
id TEXT PRIMARY KEY,
|
|
246
|
-
owner_user_id TEXT NOT NULL,
|
|
247
|
-
name TEXT NOT NULL,
|
|
248
|
-
created_at TEXT NOT NULL
|
|
249
|
-
);
|
|
250
|
-
INSERT INTO workspaces_new SELECT id, owner_user_id, name, created_at FROM workspaces;`,
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
if (hasForeignKey(db, 'channels', 'owner_user_id', 'users')) {
|
|
254
|
-
rebuilds.push({
|
|
255
|
-
table: 'channels',
|
|
256
|
-
ddl: `CREATE TABLE channels_new (
|
|
257
|
-
id TEXT PRIMARY KEY,
|
|
258
|
-
name TEXT NOT NULL,
|
|
259
|
-
description TEXT NOT NULL DEFAULT '',
|
|
260
|
-
lead_agent_id TEXT,
|
|
261
|
-
workspace TEXT NOT NULL DEFAULT '',
|
|
262
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
263
|
-
owner_user_id TEXT,
|
|
264
|
-
created_at TEXT NOT NULL,
|
|
265
|
-
updated_at TEXT NOT NULL
|
|
266
|
-
);
|
|
267
|
-
INSERT INTO channels_new SELECT id, name, description, lead_agent_id, workspace, enabled, owner_user_id, created_at, updated_at FROM channels;`,
|
|
268
|
-
})
|
|
269
|
-
}
|
|
270
|
-
if (hasForeignKey(db, 'agents', 'owner_user_id', 'users')) {
|
|
271
|
-
rebuilds.push({
|
|
272
|
-
table: 'agents',
|
|
273
|
-
ddl: `CREATE TABLE agents_new (
|
|
274
|
-
id TEXT PRIMARY KEY,
|
|
275
|
-
name TEXT NOT NULL,
|
|
276
|
-
harness TEXT NOT NULL,
|
|
277
|
-
config_json TEXT NOT NULL DEFAULT '{}',
|
|
278
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
279
|
-
visibility TEXT NOT NULL DEFAULT 'private',
|
|
280
|
-
owner_user_id TEXT,
|
|
281
|
-
created_at TEXT NOT NULL,
|
|
282
|
-
updated_at TEXT NOT NULL
|
|
283
|
-
);
|
|
284
|
-
INSERT INTO agents_new SELECT id, name, harness, config_json, enabled, visibility, owner_user_id, created_at, updated_at FROM agents;`,
|
|
285
|
-
})
|
|
286
|
-
}
|
|
287
|
-
if (hasForeignKey(db, 'teams', 'owner_user_id', 'users')) {
|
|
288
|
-
rebuilds.push({
|
|
289
|
-
table: 'teams',
|
|
290
|
-
ddl: `CREATE TABLE teams_new (
|
|
291
|
-
id TEXT PRIMARY KEY,
|
|
292
|
-
name TEXT NOT NULL,
|
|
293
|
-
description TEXT NOT NULL DEFAULT '',
|
|
294
|
-
visibility TEXT NOT NULL DEFAULT 'private',
|
|
295
|
-
owner_user_id TEXT,
|
|
296
|
-
created_at TEXT NOT NULL,
|
|
297
|
-
updated_at TEXT NOT NULL
|
|
298
|
-
);
|
|
299
|
-
INSERT INTO teams_new SELECT id, name, description, visibility, owner_user_id, created_at, updated_at FROM teams;`,
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
if (rebuilds.length === 0) return
|
|
247
|
+
const needWorkspaces = hasForeignKey(db, 'workspaces', 'owner_user_id', 'users')
|
|
248
|
+
const needChannels = hasForeignKey(db, 'channels', 'owner_user_id', 'users')
|
|
249
|
+
const needAgents = hasForeignKey(db, 'agents', 'owner_user_id', 'users')
|
|
250
|
+
const needTeams = hasForeignKey(db, 'teams', 'owner_user_id', 'users')
|
|
251
|
+
if (!needWorkspaces && !needChannels && !needAgents && !needTeams) return
|
|
303
252
|
|
|
304
253
|
db.exec('PRAGMA foreign_keys = OFF;')
|
|
305
254
|
try {
|
|
306
|
-
|
|
307
|
-
db.
|
|
308
|
-
|
|
309
|
-
|
|
255
|
+
if (needWorkspaces) {
|
|
256
|
+
db.prepare(`CREATE TABLE workspaces_new (
|
|
257
|
+
id TEXT PRIMARY KEY,
|
|
258
|
+
owner_user_id TEXT NOT NULL,
|
|
259
|
+
name TEXT NOT NULL,
|
|
260
|
+
created_at TEXT NOT NULL
|
|
261
|
+
)`).run()
|
|
262
|
+
db.prepare('INSERT INTO workspaces_new (id, owner_user_id, name, created_at) SELECT id, owner_user_id, name, created_at FROM workspaces').run()
|
|
263
|
+
db.prepare('DROP TABLE workspaces').run()
|
|
264
|
+
db.prepare('ALTER TABLE workspaces_new RENAME TO workspaces').run()
|
|
265
|
+
}
|
|
266
|
+
if (needChannels) {
|
|
267
|
+
db.prepare(`CREATE TABLE channels_new (
|
|
268
|
+
id TEXT PRIMARY KEY,
|
|
269
|
+
name TEXT NOT NULL,
|
|
270
|
+
description TEXT NOT NULL DEFAULT '',
|
|
271
|
+
scenario_prompt TEXT NOT NULL DEFAULT '',
|
|
272
|
+
llm_json TEXT NOT NULL DEFAULT '',
|
|
273
|
+
lead_agent_id TEXT,
|
|
274
|
+
workspace TEXT NOT NULL DEFAULT '',
|
|
275
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
276
|
+
created_at TEXT NOT NULL,
|
|
277
|
+
updated_at TEXT NOT NULL,
|
|
278
|
+
owner_user_id TEXT,
|
|
279
|
+
visibility TEXT NOT NULL DEFAULT 'private',
|
|
280
|
+
join_policy TEXT NOT NULL DEFAULT 'owner_approve',
|
|
281
|
+
approval_policy TEXT NOT NULL DEFAULT 'owner_only',
|
|
282
|
+
chat_enabled INTEGER NOT NULL DEFAULT 0,
|
|
283
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
284
|
+
line_id TEXT NOT NULL DEFAULT ''
|
|
285
|
+
)`).run()
|
|
286
|
+
db.prepare(`INSERT INTO channels_new (id, name, description, scenario_prompt, llm_json, lead_agent_id,
|
|
287
|
+
workspace, enabled, created_at, updated_at, owner_user_id, visibility,
|
|
288
|
+
join_policy, approval_policy, chat_enabled, version, line_id)
|
|
289
|
+
SELECT id, name, description, scenario_prompt, llm_json, lead_agent_id,
|
|
290
|
+
workspace, enabled, created_at, updated_at, owner_user_id, visibility,
|
|
291
|
+
join_policy, approval_policy, chat_enabled, version, line_id FROM channels`).run()
|
|
292
|
+
db.prepare('DROP TABLE channels').run()
|
|
293
|
+
db.prepare('ALTER TABLE channels_new RENAME TO channels').run()
|
|
294
|
+
}
|
|
295
|
+
if (needAgents) {
|
|
296
|
+
db.prepare(`CREATE TABLE agents_new (
|
|
297
|
+
id TEXT PRIMARY KEY,
|
|
298
|
+
name TEXT NOT NULL,
|
|
299
|
+
harness TEXT NOT NULL,
|
|
300
|
+
config_json TEXT NOT NULL DEFAULT '{}',
|
|
301
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
302
|
+
visibility TEXT NOT NULL DEFAULT 'private',
|
|
303
|
+
created_at TEXT NOT NULL,
|
|
304
|
+
updated_at TEXT NOT NULL,
|
|
305
|
+
owner_user_id TEXT
|
|
306
|
+
)`).run()
|
|
307
|
+
db.prepare('INSERT INTO agents_new (id, name, harness, config_json, enabled, visibility, created_at, updated_at, owner_user_id) SELECT id, name, harness, config_json, enabled, visibility, created_at, updated_at, owner_user_id FROM agents').run()
|
|
308
|
+
db.prepare('DROP TABLE agents').run()
|
|
309
|
+
db.prepare('ALTER TABLE agents_new RENAME TO agents').run()
|
|
310
|
+
}
|
|
311
|
+
if (needTeams) {
|
|
312
|
+
db.prepare(`CREATE TABLE teams_new (
|
|
313
|
+
id TEXT PRIMARY KEY,
|
|
314
|
+
name TEXT NOT NULL,
|
|
315
|
+
description TEXT NOT NULL DEFAULT '',
|
|
316
|
+
visibility TEXT NOT NULL DEFAULT 'private',
|
|
317
|
+
created_at TEXT NOT NULL,
|
|
318
|
+
updated_at TEXT NOT NULL,
|
|
319
|
+
owner_user_id TEXT
|
|
320
|
+
)`).run()
|
|
321
|
+
db.prepare('INSERT INTO teams_new (id, name, description, visibility, created_at, updated_at, owner_user_id) SELECT id, name, description, visibility, created_at, updated_at, owner_user_id FROM teams').run()
|
|
322
|
+
db.prepare('DROP TABLE teams').run()
|
|
323
|
+
db.prepare('ALTER TABLE teams_new RENAME TO teams').run()
|
|
310
324
|
}
|
|
311
|
-
|
|
325
|
+
// 重建会连带删除该表的索引;这 4 张表在 SCHEMA_SQL 里只有一个索引,原样补回
|
|
326
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_user_id)').run()
|
|
312
327
|
}
|
|
313
328
|
finally {
|
|
314
329
|
db.exec('PRAGMA foreign_keys = ON;')
|
|
@@ -7,10 +7,12 @@ import type { DcwCreateInput, DcwPatchInput } from './types'
|
|
|
7
7
|
import { AppError, ErrorCodes } from '../../../../utils/errors'
|
|
8
8
|
import { DcwNode } from '../dcw-node'
|
|
9
9
|
import { dcwKeyFromRef, normalizeDataTransform } from '../../../../../shared/dcw-protocol'
|
|
10
|
+
import { stripMaskedSecrets } from '../../../../../shared/daq-protocol'
|
|
10
11
|
import { findDcwTemplate } from '../dcw-templates'
|
|
11
12
|
import { getDcwLineRepo } from '../dcw-line.repo'
|
|
12
13
|
import { getDcwParamRepo } from '../param-map.repo'
|
|
13
14
|
import { requireDcwDriverKind } from '../drivers'
|
|
15
|
+
import { assertDriverRawRange } from '../drivers/shared'
|
|
14
16
|
import { randomUUID } from 'node:crypto'
|
|
15
17
|
|
|
16
18
|
function normalizeStepLimitInput(value: unknown): number | null | undefined {
|
|
@@ -37,7 +39,7 @@ export abstract class DcwControllerCrud extends DcwControllerControl {
|
|
|
37
39
|
templateRef: input.templateRef,
|
|
38
40
|
name: input.name ?? `${tpl.name} ${String(seq).padStart(2, '0')}`,
|
|
39
41
|
driver: input.driver ? requireDcwDriverKind(input.driver) : undefined,
|
|
40
|
-
driverConfig: input.driverConfig ?? {},
|
|
42
|
+
driverConfig: stripMaskedSecrets(input.driverConfig ?? {}),
|
|
41
43
|
transform: normalizeDataTransform(input.transform),
|
|
42
44
|
enabled: input.enabled,
|
|
43
45
|
holdIntervalMs: input.holdIntervalMs ?? null,
|
|
@@ -55,6 +57,13 @@ export abstract class DcwControllerCrud extends DcwControllerControl {
|
|
|
55
57
|
writeLockSeconds: input.writeLockSeconds,
|
|
56
58
|
stepLimit: normalizeStepLimitInput(input.stepLimit),
|
|
57
59
|
})
|
|
60
|
+
// S15:Modbus 原始量程必须在配置期可被 dataType 表示(越界运行期会拒绝写,但提前拦更好)
|
|
61
|
+
try {
|
|
62
|
+
assertDriverRawRange(node.driver, node.driverConfig)
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, err instanceof Error ? err.message : String(err))
|
|
66
|
+
}
|
|
58
67
|
this.repo.insert(node)
|
|
59
68
|
// 工艺参数映射面:节点创建即自动生成同名映射(用户/Agent 的参数语义面;
|
|
60
69
|
// PLC 寻址细节仍封装在节点驱动配置内)。失败不阻断节点创建(可事后手工补建)。
|
|
@@ -72,20 +81,34 @@ export abstract class DcwControllerCrud extends DcwControllerControl {
|
|
|
72
81
|
// 保写心跳若继续按旧值周期重下发,会把陈旧设定写向新目标(10-07 换绑残留事故根因①)。
|
|
73
82
|
// 清值后心跳自动停驻,须新写成功才重新锚定。
|
|
74
83
|
let invalidateValue = false
|
|
84
|
+
const prevDriver = node.driver
|
|
85
|
+
const prevDriverConfig = node.driverConfig
|
|
75
86
|
if (patch.name !== undefined) node.name = patch.name
|
|
76
87
|
if (patch.driver !== undefined) {
|
|
77
88
|
node.driver = requireDcwDriverKind(patch.driver)
|
|
78
89
|
invalidateValue = true
|
|
79
90
|
}
|
|
80
91
|
if (patch.driverConfig !== undefined) {
|
|
81
|
-
|
|
92
|
+
// P0-2(2026-10-28 审查):掩码回显不得覆盖真实凭据(与 DAQ 同源单点过滤)
|
|
93
|
+
node.driverConfig = { ...node.driverConfig, ...stripMaskedSecrets(patch.driverConfig) }
|
|
82
94
|
invalidateValue = true
|
|
83
95
|
}
|
|
96
|
+
// S15:配置期校验(失败回滚 driver/driverConfig,避免内存里留下半改状态)
|
|
97
|
+
try {
|
|
98
|
+
assertDriverRawRange(node.driver, node.driverConfig)
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
node.driver = prevDriver
|
|
102
|
+
node.driverConfig = prevDriverConfig
|
|
103
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, err instanceof Error ? err.message : String(err))
|
|
104
|
+
}
|
|
84
105
|
if (patch.transform !== undefined) {
|
|
85
106
|
if (patch.transform.kind === 'linear' && (!Number.isFinite(Number(patch.transform.scale)) || Number(patch.transform.scale) === 0)) {
|
|
86
107
|
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, '标定系数 scale 必须为非零数字(物理值 = scale × PLC值 + offset)')
|
|
87
108
|
}
|
|
88
109
|
node.transform = normalizeDataTransform(patch.transform)
|
|
110
|
+
// 标定变了 → 旧 value 的物理含义变了,保写心跳必须停驻到下一次成功写
|
|
111
|
+
invalidateValue = true
|
|
89
112
|
}
|
|
90
113
|
if (patch.unit !== undefined) node.unit = patch.unit
|
|
91
114
|
// decimals 必须 0..6 整数:它同时喂给 toFixed() —— 越界值会在**驱动已经写进 PLC 之后**
|
|
@@ -102,11 +125,13 @@ export abstract class DcwControllerCrud extends DcwControllerControl {
|
|
|
102
125
|
if (patch.min !== undefined) {
|
|
103
126
|
const v = Number(patch.min)
|
|
104
127
|
if (!Number.isFinite(v)) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `min 必须为有限数字(当前: ${String(patch.min)})`)
|
|
128
|
+
if (node.min !== v) invalidateValue = true
|
|
105
129
|
node.min = v
|
|
106
130
|
}
|
|
107
131
|
if (patch.max !== undefined) {
|
|
108
132
|
const v = Number(patch.max)
|
|
109
133
|
if (!Number.isFinite(v)) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `max 必须为有限数字(当前: ${String(patch.max)})`)
|
|
134
|
+
if (node.max !== v) invalidateValue = true
|
|
110
135
|
node.max = v
|
|
111
136
|
}
|
|
112
137
|
// 量程交叉校验放在两者都应用之后:min>max 会让 writeTolerance 变负、越界判定恒真
|
|
@@ -37,6 +37,10 @@ export abstract class DcwControllerLines extends DcwControllerRecipes {
|
|
|
37
37
|
if (opts?.purge) this.remove(n.id)
|
|
38
38
|
else {
|
|
39
39
|
n.lineId = ''
|
|
40
|
+
// 2026-10-28:解绑产线必须清掉当前设定 —— 否则保写心跳会以"未分配产线"身份
|
|
41
|
+
// 继续把该线最后的 setpoint 写给 PLC(旧逻辑 !lineId 默认豁免产线门)。
|
|
42
|
+
n.value = null
|
|
43
|
+
this.runtimes.get(n.id)?.rearm()
|
|
40
44
|
this.emitNodeChanged('updated', n)
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -78,10 +82,17 @@ export abstract class DcwControllerLines extends DcwControllerRecipes {
|
|
|
78
82
|
}
|
|
79
83
|
const line = getDcwLineRepo().byId(lineId)
|
|
80
84
|
if (!line) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lineId}`)
|
|
81
|
-
if (getActiveLineRun(lineId)) {
|
|
82
|
-
throw new AppError(409, ErrorCodes.CONFLICT, `产线「${line.name}」已在运行(批次 ${getActiveLineRun(lineId)!.runId}),请先停止当前数据采集`)
|
|
83
|
-
}
|
|
84
85
|
const repo = getDcwRecipeRepo()
|
|
86
|
+
// 2026-10-28:注册表可能指向一个已被关闭/不存在的批次(关闭接口/多入口/崩溃恢复竞态)。
|
|
87
|
+
// 这种情况应清掉过期窗口后继续开跑,而不是把整条线永久卡在"已在运行"。
|
|
88
|
+
const current = getActiveLineRun(lineId)
|
|
89
|
+
if (current) {
|
|
90
|
+
const dbRun = repo.runById(current.runId)
|
|
91
|
+
if (dbRun && !dbRun.endedAt) {
|
|
92
|
+
throw new AppError(409, ErrorCodes.CONFLICT, `产线「${line.name}」已在运行(批次 ${current.runId}),请先停止当前数据采集`)
|
|
93
|
+
}
|
|
94
|
+
clearActiveLineRun(lineId)
|
|
95
|
+
}
|
|
85
96
|
const recipe = repo.byId(recipeId)
|
|
86
97
|
if (!recipe) throw new AppError(404, ErrorCodes.NOT_FOUND, `Recipe 不存在: ${recipeId}`)
|
|
87
98
|
if (recipe.lineId !== lineId) {
|
|
@@ -7,12 +7,13 @@ import type { DcwWriteHistoryEntry } from '../dcw-recipe.repo'
|
|
|
7
7
|
import type { RecipeInput, RecipeRunView, RecipeView, WriteVerifyOutcome } from '../../../../../shared/dcw-protocol'
|
|
8
8
|
import { AppError, ErrorCodes } from '../../../../utils/errors'
|
|
9
9
|
import { getDcwRecipeRepo } from '../dcw-recipe.repo'
|
|
10
|
-
import { assertRecipeOpIntervalForExecution, recordRecipeOpAnchor } from '../recipe-op-anchor'
|
|
10
|
+
import { assertRecipeOpIntervalForExecution, beginRecipeOp, endRecipeOp, recordRecipeOpAnchor } from '../recipe-op-anchor'
|
|
11
11
|
import { hydrateTrialLastAt, setTrialLastAt } from './gate-persist'
|
|
12
12
|
import { recordOps } from '../../ops/ops'
|
|
13
13
|
import { summarizeAck, verifyRecipeWrite } from './write-verify'
|
|
14
14
|
import { writeTolerance } from '../dcw-runtime'
|
|
15
15
|
import { notifyLineOperators } from '../../runtime/line-notify'
|
|
16
|
+
import { assertWithinLimits } from '../param-limits'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* 试验节拍 env 解析(显式 0 = 关闭;空/非法 = 默认 5 分钟)。
|
|
@@ -111,7 +112,7 @@ export abstract class DcwControllerRecipes extends DcwControllerParams {
|
|
|
111
112
|
* 2026-10-08 ACK 鉴定:每参数写后经 verifyRecipeWrite 补验(链路受理→独立回读
|
|
112
113
|
* 证实),结果行携带 ack/verify —— 「ok」不再被当成「设备已生效」。
|
|
113
114
|
*/
|
|
114
|
-
protected async writeRecipeParams(recipe: RecipeView, run: RecipeRunView): Promise<void> {
|
|
115
|
+
protected async writeRecipeParams(recipe: RecipeView, run: RecipeRunView, opts: { enforceRecipeWindow?: boolean } = {}): Promise<void> {
|
|
115
116
|
const results: RecipeRunView['results'] = []
|
|
116
117
|
for (const param of recipe.params) {
|
|
117
118
|
const node = param.nodeId ? this.repo.byId(param.nodeId) : undefined
|
|
@@ -129,6 +130,24 @@ export abstract class DcwControllerRecipes extends DcwControllerParams {
|
|
|
129
130
|
results.push({ templateRef: node.templateRef, nodeId: node.id, ok: false, message: `已跳过:节点「${node.name}」已取消绑定(${node.lineId ? '已改挂其他产线' : '已从产线移除'}),参数未下发`, value: param.value })
|
|
130
131
|
continue
|
|
131
132
|
}
|
|
133
|
+
// 2026-10-28:trial/候选覆盖值必须再过一次配方工艺窗口。
|
|
134
|
+
// 配方路径在 write() 里 skipRecipe:true(保存值已在保存时校验),但 trial 的
|
|
135
|
+
// overrides 是新值 —— 曾在 40~48 的配方窗下把 80/120 写进 PLC。
|
|
136
|
+
if (opts.enforceRecipeWindow) {
|
|
137
|
+
try {
|
|
138
|
+
assertWithinLimits(node, param.value, { includeSoft: true, skipRecipe: false })
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
results.push({
|
|
142
|
+
templateRef: node.templateRef,
|
|
143
|
+
nodeId: node.id,
|
|
144
|
+
ok: false,
|
|
145
|
+
message: `候选值未下发(越界):${err instanceof Error ? err.message : String(err)}`,
|
|
146
|
+
value: param.value,
|
|
147
|
+
})
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
}
|
|
132
151
|
try {
|
|
133
152
|
const outcome = await this.write(node.id, param.value, run.id, { source: 'recipe', actor: 'recipe' })
|
|
134
153
|
// ACK 鉴定:命令 ok 但仅链路受理(mqtt/http-no-echo/mock)→ 独立回读补验
|
|
@@ -169,6 +188,13 @@ export abstract class DcwControllerRecipes extends DcwControllerParams {
|
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
190
|
|
|
191
|
+
/** 占位试验节拍(与 assertTrialCadence 同步调用,中间不得有 await;B-P1-2)。 */
|
|
192
|
+
private claimTrialCadence(lineId: string): void {
|
|
193
|
+
const at = Date.now()
|
|
194
|
+
this.trialLastAt.set(lineId, at)
|
|
195
|
+
setTrialLastAt(lineId, at) // P1-5 落盘
|
|
196
|
+
}
|
|
197
|
+
|
|
172
198
|
/**
|
|
173
199
|
* Recipe 手动应用 / Agent 试验(一键下发工艺参数集;不激活产线窗口)。
|
|
174
200
|
* v19 trial 语义:**声明式覆盖集** —— 提供 overrides 时只写 overrides 列出的节点
|
|
@@ -197,9 +223,41 @@ export abstract class DcwControllerRecipes extends DcwControllerParams {
|
|
|
197
223
|
if (unknown.length) {
|
|
198
224
|
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `覆盖参数不在配方内: ${unknown.map(o => o.nodeId).join(', ')}(试验只能修改配方已有参数的值;新增参数请先 recipe_update)`)
|
|
199
225
|
}
|
|
200
|
-
if (opts.trial)
|
|
226
|
+
if (opts.trial) {
|
|
227
|
+
// B-P1-2(2026-10-28 审查):判定与占位必须同一次同步执行(中间无 await),
|
|
228
|
+
// 否则并发 trial 双双过检;写入完成前第二次会被节拍拦下。
|
|
229
|
+
this.assertTrialCadence(recipe.lineId)
|
|
230
|
+
this.claimTrialCadence(recipe.lineId)
|
|
231
|
+
}
|
|
201
232
|
// Recipe 级下发间隔卡控(执行时兜底):仅 Agent 路径;approvalId 自豁免防审批竞态
|
|
202
|
-
if (opts.agentDispatch)
|
|
233
|
+
if (opts.agentDispatch) {
|
|
234
|
+
assertRecipeOpIntervalForExecution(recipe, { approvalId: opts.agentApprovalId, emergency: opts.emergency })
|
|
235
|
+
// 在飞占位:频控是"查-做-记"三段,首尾之间隔着整批写入(秒级)。两个并发下发
|
|
236
|
+
// 都会在"查"时看到旧锚。占位与判定同步完成,follow 到 finally 释放。
|
|
237
|
+
beginRecipeOp(recipeId, recipe.name)
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return await this.applyRecipeLocked(recipe, opts, overrides)
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
if (opts.agentDispatch) endRecipeOp(recipeId)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** applyRecipe 的持锁段(调用方已通过节拍/频控判定并在飞占位;不得在此重复占位)。 */
|
|
248
|
+
private async applyRecipeLocked(
|
|
249
|
+
recipe: RecipeView,
|
|
250
|
+
opts: {
|
|
251
|
+
overrides?: Array<{ nodeId: string, value: number }>
|
|
252
|
+
trial?: boolean
|
|
253
|
+
agentDispatch?: boolean
|
|
254
|
+
agentApprovalId?: string
|
|
255
|
+
emergency?: boolean
|
|
256
|
+
},
|
|
257
|
+
overrides: Array<{ nodeId: string, value: number }>,
|
|
258
|
+
): Promise<RecipeRunView> {
|
|
259
|
+
const repo = getDcwRecipeRepo()
|
|
260
|
+
const recipeId = recipe.id
|
|
203
261
|
// 声明式覆盖集:有 overrides 只写声明的节点(未声明参数保持现状,绝不顺带整批);
|
|
204
262
|
// 无 overrides(手动应用/统一回退重下发)= 整批语义
|
|
205
263
|
const effective = overrides.length > 0
|
|
@@ -209,7 +267,7 @@ export abstract class DcwControllerRecipes extends DcwControllerParams {
|
|
|
209
267
|
})
|
|
210
268
|
: recipe.params
|
|
211
269
|
const run = repo.createRun(recipe, effective)
|
|
212
|
-
await this.writeRecipeParams({ ...recipe, params: effective }, run)
|
|
270
|
+
await this.writeRecipeParams({ ...recipe, params: effective }, run, { enforceRecipeWindow: overrides.length > 0 })
|
|
213
271
|
// 运行中应用新配方版本 → 谱系拆段(A13):旧段在 now 闭合,新段记录新版本/内容 hash。
|
|
214
272
|
try {
|
|
215
273
|
const { getActiveLineRun, setActiveLineRun } = await import('../line-run')
|
|
@@ -357,7 +357,16 @@ class DcwRecipeRepo {
|
|
|
357
357
|
markGood(recipeId: string, runId: string): RecipeView {
|
|
358
358
|
const r = this.byId(recipeId)
|
|
359
359
|
if (!r) throw new AppError(404, ErrorCodes.NOT_FOUND, `Recipe 不存在: ${recipeId}`)
|
|
360
|
-
|
|
360
|
+
const run = this.runById(runId)
|
|
361
|
+
if (!run) throw new AppError(404, ErrorCodes.NOT_FOUND, `批次不存在: ${runId}`)
|
|
362
|
+
// 2026-10-28:此前只校验 run 存在 —— 可把同线另一个配方/别的产品的批次标为"已知良好",
|
|
363
|
+
// 之后 to_last_good 会把那个批次的 setpoints 整批写进当前配方。
|
|
364
|
+
if (run.recipeId !== recipeId) {
|
|
365
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `批次 ${runId} 属于配方 ${run.recipeId},不能标记为配方 ${recipeId} 的良好批次`)
|
|
366
|
+
}
|
|
367
|
+
if (run.lineId !== r.lineId) {
|
|
368
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `批次 ${runId} 属于产线 ${run.lineId},与配方产线 ${r.lineId} 不一致`)
|
|
369
|
+
}
|
|
361
370
|
r.lastGoodRunId = runId
|
|
362
371
|
this.flushRecipes()
|
|
363
372
|
return r
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { AppError, ErrorCodes } from '../../../utils/errors'
|
|
10
10
|
import type { DcwNode } from './dcw-node'
|
|
11
|
+
import { assertWithinLimits } from './param-limits'
|
|
11
12
|
|
|
12
13
|
/** 回读死区容差:max(0.5×10^-小数位, 量程 0.5%) */
|
|
13
14
|
export function writeTolerance(node: DcwNode): number {
|
|
@@ -49,6 +50,8 @@ export interface DcwReadOutcome {
|
|
|
49
50
|
export class DcwNodeRuntime {
|
|
50
51
|
private writing = false
|
|
51
52
|
private lastHoldAt = 0
|
|
53
|
+
/** 保写被限界拒绝的最近告警时刻(节流,避免每拍刷日志) */
|
|
54
|
+
private holdBlockedAt = 0
|
|
52
55
|
/** 周期读节拍 + 在飞互斥(读事务不与写事务互斥,但同节点读不重入) */
|
|
53
56
|
private lastReadTick = 0
|
|
54
57
|
private reading = false
|
|
@@ -107,20 +110,40 @@ export class DcwNodeRuntime {
|
|
|
107
110
|
const hold = node.holdIntervalMs ?? this.host.defaults().holdIntervalMs
|
|
108
111
|
// P0-1 保写产线门:绑定产线的节点仅在产线活动(有运行批次窗口)时保写,停线即挂起,
|
|
109
112
|
// 开线后下一拍自然恢复(10-07 事故根因④:停线不挂起心跳,残留节点持续直写)。
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
// 2026-10-28:未分配产线的节点不再默认豁免 —— 删线(purge=false)会把 lineId 置空,
|
|
114
|
+
// 旧逻辑因此让已下线产线的设定值被永久重写。现在必须显式 AW_HOLD_UNASSIGNED=1 才保写。
|
|
115
|
+
const lineGate = node.lineId ? this.host.lineActive(node.lineId) : process.env.AW_HOLD_UNASSIGNED === '1'
|
|
116
|
+
if (hold && hold > 0 && now - this.lastHoldAt >= hold && lineGate) {
|
|
117
|
+
// 2026-10-28:保写心跳也过四层限界(含配方窗)。此前只查在飞闸门,操作员收窄量程/
|
|
118
|
+
// 换配方后可写被拒、心跳却按旧值持续重写 PLC。
|
|
119
|
+
let withinLimits = true
|
|
120
|
+
try {
|
|
121
|
+
assertWithinLimits(node, node.value!, { includeSoft: true, skipRecipe: false })
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
withinLimits = false
|
|
125
|
+
if (now - this.holdBlockedAt > 60_000) {
|
|
126
|
+
this.holdBlockedAt = now
|
|
127
|
+
console.warn(`[dcw] 保写心跳被限界拒绝(跳过本拍):${node.name}(${node.id}) ${err instanceof Error ? err.message : String(err)}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!withinLimits) {
|
|
131
|
+
// 不写,但周期读继续(下方读路径与本块独立)
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.lastHoldAt = now
|
|
135
|
+
this.writing = true
|
|
136
|
+
node.state = 'writing'
|
|
137
|
+
// tick 是同步节拍(由 sweep 同步调用),这里 fire-and-forget 但**全程持锁**,
|
|
138
|
+
// 保证与 write() 互斥;finally 里收敛状态并释放闸门。
|
|
139
|
+
void Promise.resolve()
|
|
140
|
+
.then(() => this.host.executeWrite(node, node.value!, writeTolerance(node), null))
|
|
141
|
+
.catch(() => { /* 保写失败不抛出:下一拍重试 */ })
|
|
142
|
+
.finally(() => {
|
|
143
|
+
this.writing = false
|
|
144
|
+
if (node.state === 'writing') node.state = 'error'
|
|
145
|
+
})
|
|
146
|
+
}
|
|
124
147
|
}
|
|
125
148
|
}
|
|
126
149
|
// 周期读:被动观测,不依赖写过、不与写在飞互斥(仅同节点读串行)。
|
|
@@ -164,6 +164,8 @@ export const num = (v: unknown): number | undefined => {
|
|
|
164
164
|
/**
|
|
165
165
|
* 工程量 → 原始值:线性映射 eng ∈ [engMin, engMax] ↔ raw ∈ [rawMin, rawMax]。
|
|
166
166
|
* 未提供原始量程时 raw = eng(float32 直写;int 类型仍需映射,缺省按量程直传并取整)。
|
|
167
|
+
* 2026-10-28(S15):换算结果必须落在 rawMin~rawMax 内 —— 此前越界值会被 encodeWords
|
|
168
|
+
* 静默按位回绕(int16/uint16)后写进 PLC,造成"写失败但设备已变"。
|
|
167
169
|
*/
|
|
168
170
|
export function engToRaw(eng: number, input: DcwWriteInput): number {
|
|
169
171
|
const cfg = input.driverConfig
|
|
@@ -173,7 +175,14 @@ export function engToRaw(eng: number, input: DcwWriteInput): number {
|
|
|
173
175
|
const engMin = num(cfg.engMin) ?? input.domain.min
|
|
174
176
|
const engMax = num(cfg.engMax) ?? input.domain.max
|
|
175
177
|
if (engMax === engMin) return eng
|
|
176
|
-
|
|
178
|
+
const out = rawMin + ((eng - engMin) / (engMax - engMin)) * (rawMax - rawMin)
|
|
179
|
+
if (!Number.isFinite(out)) throw new Error(`工程量 ${eng} 换算原始值失败(结果非有限数)`)
|
|
180
|
+
const lo = Math.min(rawMin, rawMax)
|
|
181
|
+
const hi = Math.max(rawMin, rawMax)
|
|
182
|
+
if (out < lo - 1e-9 || out > hi + 1e-9) {
|
|
183
|
+
throw new Error(`工程量 ${eng} 换算原始值 ${Number(out.toFixed(4))} 超出驱动原始量程 ${lo}~${hi}(eng ${engMin}~${engMax});请核对节点量程与 engMin/engMax/rawMin/rawMax 配置`)
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
/** 原始值 → 工程量(回读换算;映射参数对称) */
|
|
@@ -208,16 +217,63 @@ export function readbackAck(
|
|
|
208
217
|
return { ok, message, raw: opts.raw, readback, ack: ok ? 'readback-verified' : 'unverified' }
|
|
209
218
|
}
|
|
210
219
|
|
|
211
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* 原始值 → 寄存器字序(数据类型 + 字节序;与数采 decodeRegisters 互逆)。
|
|
222
|
+
* 2026-10-28(S15):整数类型先校验可表示范围,越界抛错而不是按位回绕。
|
|
223
|
+
*/
|
|
212
224
|
export function encodeWords(raw: number, dataType: string, byteOrder: string): number[] {
|
|
213
|
-
if (dataType === 'int16' || dataType === 'uint16')
|
|
225
|
+
if (dataType === 'int16' || dataType === 'uint16') {
|
|
226
|
+
const r = Math.round(raw)
|
|
227
|
+
if (!Number.isFinite(r)) throw new Error(`原始值 ${raw} 不是有限数,拒绝编码为 ${dataType}`)
|
|
228
|
+
if (dataType === 'int16' && (r < -32768 || r > 32767)) throw new Error(`原始值 ${r} 超出 int16 范围(-32768~32767),拒绝写入`)
|
|
229
|
+
if (dataType === 'uint16' && (r < 0 || r > 65535)) throw new Error(`原始值 ${r} 超出 uint16 范围(0~65535),拒绝写入`)
|
|
230
|
+
return [r & 0xFFFF]
|
|
231
|
+
}
|
|
232
|
+
if (dataType === 'uint32') {
|
|
233
|
+
const r = Math.round(raw)
|
|
234
|
+
if (!Number.isFinite(r) || r < 0 || r > 4294967295) throw new Error(`原始值 ${raw} 超出 uint32 范围,拒绝写入`)
|
|
235
|
+
}
|
|
236
|
+
if (dataType === 'int32') {
|
|
237
|
+
const r = Math.round(raw)
|
|
238
|
+
if (!Number.isFinite(r) || r < -2147483648 || r > 2147483647) throw new Error(`原始值 ${raw} 超出 int32 范围,拒绝写入`)
|
|
239
|
+
}
|
|
214
240
|
const buf = Buffer.alloc(4)
|
|
215
241
|
if (dataType === 'float32') buf.writeFloatBE(raw, 0)
|
|
216
|
-
else if (dataType === 'uint32') buf.writeUInt32BE(raw, 0)
|
|
217
|
-
else buf.writeInt32BE(raw, 0)
|
|
242
|
+
else if (dataType === 'uint32') buf.writeUInt32BE(Math.round(raw), 0)
|
|
243
|
+
else buf.writeInt32BE(Math.round(raw), 0)
|
|
218
244
|
const B = [buf[0]!, buf[1]!, buf[2]!, buf[3]!]
|
|
219
245
|
const word = (hi: number, lo: number) => ((hi & 0xFF) << 8) | (lo & 0xFF)
|
|
220
|
-
|
|
246
|
+
// 与 daq/drivers/modbus-tcp.ts:decodeRegisters 严格互逆(2026-10-28 P0-1):
|
|
247
|
+
// little=DCBA → [D C][B A];wordSwap=CDAB → [C D][A B];big=ABCD → [A B][C D]
|
|
248
|
+
if (byteOrder === 'little') return [word(B[3]!, B[2]!), word(B[1]!, B[0]!)]
|
|
221
249
|
if (byteOrder === 'wordSwap') return [word(B[2]!, B[3]!), word(B[0]!, B[1]!)]
|
|
222
250
|
return [word(B[0]!, B[1]!), word(B[2]!, B[3]!)]
|
|
223
251
|
}
|
|
252
|
+
|
|
253
|
+
const INT_RANGES: Record<string, [number, number]> = {
|
|
254
|
+
int16: [-32768, 32767],
|
|
255
|
+
uint16: [0, 65535],
|
|
256
|
+
int32: [-2147483648, 2147483647],
|
|
257
|
+
uint32: [0, 4294967295],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 配置期校验(S15):Modbus 原始量程必须能被 dataType 表示。
|
|
262
|
+
* 在节点创建/编辑时拒绝,避免运行期写坏 PLC 后才发现。
|
|
263
|
+
*/
|
|
264
|
+
export function assertDriverRawRange(driver: string, cfg: Record<string, unknown> = {}): void {
|
|
265
|
+
if (!driver.startsWith('modbus')) return
|
|
266
|
+
const rawMin = num(cfg.rawMin)
|
|
267
|
+
const rawMax = num(cfg.rawMax)
|
|
268
|
+
if (rawMin === undefined && rawMax === undefined) return
|
|
269
|
+
if (rawMin === undefined || rawMax === undefined) throw new Error('rawMin 与 rawMax 必须同时提供')
|
|
270
|
+
if (!(rawMax > rawMin)) throw new Error('rawMin 必须小于 rawMax')
|
|
271
|
+
const dt = String(cfg.dataType ?? 'float32')
|
|
272
|
+
const range = INT_RANGES[dt]
|
|
273
|
+
if (range && (rawMin < range[0] || rawMax > range[1])) {
|
|
274
|
+
throw new Error(`原始量程 ${rawMin}~${rawMax} 超出 ${dt} 可表示范围 ${range[0]}~${range[1]}`)
|
|
275
|
+
}
|
|
276
|
+
const engMin = num(cfg.engMin)
|
|
277
|
+
const engMax = num(cfg.engMax)
|
|
278
|
+
if (engMin !== undefined && engMax !== undefined && !(engMax > engMin)) throw new Error('engMin 必须小于 engMax')
|
|
279
|
+
}
|
|
@@ -67,6 +67,34 @@ export function getRecipeOpAnchor(recipeId: string): RecipeOpAnchor | null {
|
|
|
67
67
|
return store().anchors.find(a => a.recipeId === recipeId) ?? null
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 执行在飞占位(2026-10-28 审查 B-P1-2)。
|
|
72
|
+
*
|
|
73
|
+
* 频控判定是"查锚 → 写入(秒级 await)→ 落锚"三段:两个并发下发都会在"查"时
|
|
74
|
+
* 读到同一旧锚而双双过检,opIntervalMs/试验节拍被整体绕过。占位与判定必须在
|
|
75
|
+
* 同一次同步执行内完成(JS 单线程内天然原子),写入结束在 finally 释放。
|
|
76
|
+
* 跨进程语义仍由单进程写锁约束(与本仓库其它写路径同口径)。
|
|
77
|
+
*/
|
|
78
|
+
const inflight = new Set<string>()
|
|
79
|
+
|
|
80
|
+
export function beginRecipeOp(recipeId: string, recipeName: string): void {
|
|
81
|
+
if (inflight.has(recipeId)) {
|
|
82
|
+
throw new AppError(429, ErrorCodes.WRITE_FREQUENT,
|
|
83
|
+
`配方「${recipeName}」已有一次下发正在执行(在飞占位),本次未获执行、产线未做任何变更。`
|
|
84
|
+
+ `请等待该次下发返回后重试;若长时间无响应,请检查产线连接与写历史。`)
|
|
85
|
+
}
|
|
86
|
+
inflight.add(recipeId)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function endRecipeOp(recipeId: string): void {
|
|
90
|
+
inflight.delete(recipeId)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 仅供测试/观测:该配方是否有在飞下发。 */
|
|
94
|
+
export function isRecipeOpInflight(recipeId: string): boolean {
|
|
95
|
+
return inflight.has(recipeId)
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
/** 仅供测试:清空全部锚(内存+落盘) */
|
|
71
99
|
export function resetRecipeOpAnchors(): void {
|
|
72
100
|
g.__recipeOpAnchors = { anchors: [] }
|