agentworkshop 0.7.39 → 0.7.40
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.json +55 -0
- package/.output/.AgentWorkShop/prompts/host-tools.json +55 -0
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/88GOowgO2.js +1 -0
- package/.output/public/_nuxt/{BNT8gYU92.js → BC9wjTJK2.js} +2 -2
- package/.output/public/_nuxt/BOt2SP4z.js +1 -0
- package/.output/public/_nuxt/BP-NyKVG.js +18 -0
- package/.output/public/_nuxt/{HBLJ-wei.js → BXxH0UQE.js} +1 -1
- package/.output/public/_nuxt/{zIGh7R49.js → BneN7fbu.js} +1 -1
- package/.output/public/_nuxt/BsROmY89.js +1 -0
- package/.output/public/_nuxt/{C_OLzD09.js → C4oW1HsE.js} +1 -1
- package/.output/public/_nuxt/CFxtLwjp.js +1 -0
- package/.output/public/_nuxt/{Cec0Ua3F.js → CS50LB_l.js} +1 -1
- package/.output/public/_nuxt/C_iYD-EB.js +1 -0
- package/.output/public/_nuxt/{DAe0h2MB.js → ClILjvWH.js} +1 -1
- package/.output/public/_nuxt/CssgYAAL2.js +1 -0
- package/.output/public/_nuxt/{CY9LbPCI.js → CzOCSGLe.js} +1 -1
- package/.output/public/_nuxt/D8qvkfyY.js +1 -0
- package/.output/public/_nuxt/DCn7QZ5N.js +2 -0
- package/.output/public/_nuxt/DGIuvyRN2.js +1 -0
- package/.output/public/_nuxt/{CojCTLKD.js → DGj-WEBg.js} +1 -1
- package/.output/public/_nuxt/{DDShH71m.js → DS8qj8EL.js} +1 -1
- package/.output/public/_nuxt/DTFCy6h-.js +1 -0
- package/.output/public/_nuxt/{DrYV5i68.js → DUjW5Yq5.js} +1 -1
- package/.output/public/_nuxt/DZshyNLa.js +1 -0
- package/.output/public/_nuxt/{45zuyAso.js → D_OTvQza.js} +1 -1
- package/.output/public/_nuxt/DcsCcxp4.js +1 -0
- package/.output/public/_nuxt/{DUrecWPx.js → Dihnimlk.js} +2 -2
- package/.output/public/_nuxt/Dqw1lpr-.js +1 -0
- package/.output/public/_nuxt/{CUDwzgLV.js → Dr9v6C0f.js} +9 -9
- package/.output/public/_nuxt/{8NMOXZOV.js → Du9ZJtnj.js} +1 -1
- package/.output/public/_nuxt/TownView.f-YOdXRs.css +1 -0
- package/.output/public/_nuxt/_id_.CM4bIgKY.css +1 -0
- package/.output/public/_nuxt/_id_.bNV69Dtf.css +1 -0
- package/.output/public/_nuxt/_wsId_.B4WiqkIS.css +1 -0
- package/.output/public/_nuxt/agents.ZD0rVdHE.css +1 -0
- package/.output/public/_nuxt/aml.BN8ZEW4h.css +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/f4707884-7cf4-4a31-80d2-e3d72b73745f.json +1 -0
- package/.output/public/_nuxt/channel-templates.BOtahzPi.css +1 -0
- package/.output/public/_nuxt/daq.B0D3dPAc.css +1 -0
- package/.output/public/_nuxt/dcw.8Y7FqCVN.css +1 -0
- package/.output/public/_nuxt/default.J32SgqFY.css +1 -0
- package/.output/public/_nuxt/entry.bdm0tv6M.css +1 -0
- package/.output/public/_nuxt/logs.BpKzmKsh.css +1 -0
- package/.output/public/_nuxt/monitor.D9pe7qN6.css +1 -0
- package/.output/public/_nuxt/pages.D7aSDlUI.css +1 -0
- package/.output/public/_nuxt/permissions.DwTVlFe6.css +1 -0
- package/.output/public/_nuxt/plugins.sb4nDGqr.css +1 -0
- package/.output/public/_nuxt/{Cbq-mryP.js → qr6R6Ha1.js} +1 -1
- package/.output/public/_nuxt/settings.BTgR-NiE.css +1 -0
- package/.output/public/_nuxt/teams.CxnxKVVs.css +1 -0
- package/.output/public/_nuxt/tokens.BFY7qv4f.css +1 -0
- package/.output/public/_nuxt/{town.DKIAhKnx.css → town.BBXZmeoP.css} +1 -1
- package/.output/public/_nuxt/users.DLgZ1D0h.css +1 -0
- package/.output/public/_nuxt/workshop.CR_QzeAN.css +1 -0
- package/.output/public/_nuxt/{BzHEilTt.js → xh3_vYcu.js} +1 -1
- package/.output/public/_nuxt/yF0aYMF1.js +4 -0
- package/.output/server/chunks/_/entity.mjs.map +1 -1
- package/.output/server/chunks/_/nitro.mjs +2225 -1445
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{TownView-jVazYOjN.mjs → TownView-BuLAsmpR.mjs} +288 -206
- package/.output/server/chunks/build/TownView-BuLAsmpR.mjs.map +1 -0
- package/.output/server/chunks/build/{_id_-CfVosNg3.mjs → _id_-BU1wfOID.mjs} +138 -138
- package/.output/server/chunks/build/{_id_-CfVosNg3.mjs.map → _id_-BU1wfOID.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-Hwr4rZpr.mjs → _id_-BYsEua57.mjs} +33 -32
- package/.output/server/chunks/build/_id_-BYsEua57.mjs.map +1 -0
- package/.output/server/chunks/build/{_wsId_-BVg3bB77.mjs → _wsId_-BxlDcJHg.mjs} +293 -259
- package/.output/server/chunks/build/{_wsId_-BVg3bB77.mjs.map → _wsId_-BxlDcJHg.mjs.map} +1 -1
- package/.output/server/chunks/build/{agents-Bvin73QY.mjs → agents-CLQqJF2y.mjs} +21 -27
- package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +1 -0
- package/.output/server/chunks/build/{aml-BCSJCdX7.mjs → aml-_7FdFL2p.mjs} +109 -109
- package/.output/server/chunks/build/{aml-BCSJCdX7.mjs.map → aml-_7FdFL2p.mjs.map} +1 -1
- package/.output/server/chunks/build/api-error-dul30BYQ.mjs.map +1 -1
- package/.output/server/chunks/build/{channel-templates-BTq8570n.mjs → channel-templates-p6SZNjlM.mjs} +19 -19
- package/.output/server/chunks/build/{channel-templates-BTq8570n.mjs.map → channel-templates-p6SZNjlM.mjs.map} +1 -1
- package/.output/server/chunks/build/{daq-C03Uu5UE.mjs → daq-cgyjKmJu.mjs} +188 -122
- package/.output/server/chunks/build/daq-cgyjKmJu.mjs.map +1 -0
- package/.output/server/chunks/build/{dcw-6vEYVeje.mjs → dcw-CoTxBlAZ.mjs} +45 -45
- package/.output/server/chunks/build/{dcw-6vEYVeje.mjs.map → dcw-CoTxBlAZ.mjs.map} +1 -1
- package/.output/server/chunks/build/{default-CZmZFpqH.mjs → default-BE4r0FZN.mjs} +152 -58
- package/.output/server/chunks/build/{default-CZmZFpqH.mjs.map → default-BE4r0FZN.mjs.map} +1 -1
- package/.output/server/chunks/build/{logs-B-xhuqln.mjs → logs-BSOeZwwI.mjs} +34 -34
- package/.output/server/chunks/build/{logs-B-xhuqln.mjs.map → logs-BSOeZwwI.mjs.map} +1 -1
- package/.output/server/chunks/build/{monitor-CyfDt4nR.mjs → monitor-C0okQaXz.mjs} +33 -33
- package/.output/server/chunks/build/{monitor-CyfDt4nR.mjs.map → monitor-C0okQaXz.mjs.map} +1 -1
- package/.output/server/chunks/build/{pages-BO7yZsXA.mjs → pages-DFxjn9Xa.mjs} +33 -17
- package/.output/server/chunks/build/pages-DFxjn9Xa.mjs.map +1 -0
- package/.output/server/chunks/build/{permissions-B52oOmaO.mjs → permissions-68SsNjex.mjs} +12 -12
- package/.output/server/chunks/build/{permissions-B52oOmaO.mjs.map → permissions-68SsNjex.mjs.map} +1 -1
- package/.output/server/chunks/build/{plugins-CUWmMdyt.mjs → plugins-lGlUr1lj.mjs} +17 -17
- package/.output/server/chunks/build/{plugins-CUWmMdyt.mjs.map → plugins-lGlUr1lj.mjs.map} +1 -1
- package/.output/server/chunks/build/{settings-DoQ2Oq3P.mjs → settings-Ddd1qx2Z.mjs} +50 -47
- package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +1 -0
- package/.output/server/chunks/build/{teams-2kz3CusY.mjs → teams-MEDLZdMn.mjs} +38 -34
- package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +1 -0
- package/.output/server/chunks/build/{tokens-BbWLqtVz.mjs → tokens-C7fTM5C2.mjs} +14 -14
- package/.output/server/chunks/build/{tokens-BbWLqtVz.mjs.map → tokens-C7fTM5C2.mjs.map} +1 -1
- package/.output/server/chunks/build/{town-BOl6hs9B.mjs → town-qMPql9Se.mjs} +36 -10
- package/.output/server/chunks/build/town-qMPql9Se.mjs.map +1 -0
- package/.output/server/chunks/build/useDaqStream-CIBaiGAl.mjs +1 -1
- package/.output/server/chunks/build/useDcwStream-CBbiYnWe.mjs +1 -1
- package/.output/server/chunks/build/useResponsive-BKDMGTHs.mjs +49 -0
- package/.output/server/chunks/build/useResponsive-BKDMGTHs.mjs.map +1 -0
- package/.output/server/chunks/build/{users-fohSmF-J.mjs → users-Cfd5-l2-.mjs} +6 -6
- package/.output/server/chunks/build/{users-fohSmF-J.mjs.map → users-Cfd5-l2-.mjs.map} +1 -1
- package/.output/server/chunks/build/{workshop-DX5-ldc4.mjs → workshop-BL5bhfs0.mjs} +15 -15
- package/.output/server/chunks/build/{workshop-DX5-ldc4.mjs.map → workshop-BL5bhfs0.mjs.map} +1 -1
- package/.output/server/chunks/build/{workspaces-BuydxM-3.mjs → workspaces-Bgz5Ekwp.mjs} +18 -3
- package/.output/server/chunks/build/workspaces-Bgz5Ekwp.mjs.map +1 -0
- package/.output/server/chunks/routes/api/system/config-groups/_id_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/system/index.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/system/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/system/settings/events.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/agent-tools/invoke.post.mjs +6 -2
- package/.output/server/chunks/routes/api/workshop/agent-tools/invoke.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/aml/datasets/_id_.get.mjs +2 -1
- package/.output/server/chunks/routes/api/workshop/aml/datasets/_id_.get.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/channels.post.mjs +3 -3
- package/.output/server/chunks/routes/api/workshop/daq/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/write.post.mjs +3 -2
- package/.output/server/chunks/routes/api/workshop/dcw/_id/write.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.delete.mjs +2 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index2.post.mjs +8 -6
- package/.output/server/chunks/routes/api/workshop/dcw/index2.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index3.post.mjs +5 -5
- package/.output/server/chunks/routes/api/workshop/dcw/index3.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs +14 -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 +4 -4
- package/.output/server/chunks/routes/api/workshop/dcw/index4.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index5.get.mjs +4 -3
- package/.output/server/chunks/routes/api/workshop/dcw/index5.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +31 -0
- package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/index6.get.mjs +28 -0
- package/.output/server/chunks/routes/api/workshop/dcw/index6.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id_.patch.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/read.post.mjs +32 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/read.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/write.post.mjs +34 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id/write.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +33 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +34 -0
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs.map +1 -0
- 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/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.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index6.get.mjs +2 -0
- package/.output/server/chunks/routes/api/workshop/index6.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/index9.get.mjs +3 -1
- package/.output/server/chunks/routes/api/workshop/index9.get.mjs.map +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/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/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs +3 -3
- package/.output/server/chunks/virtual/entry.mjs +51 -25
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README-zh.md +199 -35
- package/README.md +739 -591
- package/app/app.vue +9 -3
- package/app/assets/css/main.css +517 -7
- package/app/components/AppHeader.vue +98 -7
- package/app/components/AppSidebar.vue +117 -11
- package/app/components/AwChart.vue +2 -1
- package/app/components/workshop/AgentInspectorDrawer.vue +6 -6
- package/app/components/workshop/AgentLanesView.vue +52 -1
- package/app/components/workshop/AssetLibrary.vue +4 -2
- package/app/components/workshop/ChannelSessionList.vue +4 -2
- package/app/components/workshop/Composer.vue +96 -0
- package/app/components/workshop/DaqNodeRow.vue +19 -0
- package/app/components/workshop/DaqTemplateIcon.vue +33 -0
- package/app/components/workshop/DeviceTwinPanel.vue +37 -35
- package/app/components/workshop/EventBlock.vue +3 -3
- package/app/components/workshop/InspectorPanel.vue +47 -11
- package/app/components/workshop/LiveAgentBar.vue +34 -0
- package/app/components/workshop/TaskBoardView.vue +83 -33
- package/app/components/workshop/TranscriptTimeline.vue +56 -0
- package/app/components/workshop/blocks/ClusterError.vue +1 -1
- package/app/components/workshop/blocks/ClusterRoute.vue +3 -1
- package/app/components/workshop/blocks/ClusterStatus.vue +2 -2
- package/app/components/workshop/blocks/ClusterStream.vue +8 -2
- package/app/components/workshop/town/ModelPreview3D.vue +1 -1
- package/app/components/workshop/town/TownView.vue +507 -82
- package/app/composables/useResponsive.ts +88 -0
- package/app/composables/useRouteMeta.ts +3 -0
- package/app/composables/workshop/useDcwStream.ts +1 -1
- package/app/composables/workshop/useVisibleInterval.ts +2 -2
- package/app/composables/workshop/useWorkshopApi.ts +1 -1
- package/app/layouts/default.vue +46 -2
- package/app/pages/aml/index.vue +712 -616
- package/app/pages/daq/[id].vue +3 -2
- package/app/pages/daq/index.vue +209 -9
- package/app/pages/dcw/[id].vue +109 -28
- package/app/pages/dcw/index.vue +83 -23
- package/app/pages/index.vue +35 -1
- package/app/pages/logs/index.vue +147 -13
- package/app/pages/monitor.vue +56 -12
- package/app/pages/permissions/index.vue +34 -0
- package/app/pages/plugins/index.vue +35 -4
- package/app/pages/settings.vue +90 -21
- package/app/pages/tokens.vue +23 -0
- package/app/pages/town.vue +50 -5
- package/app/pages/users.vue +23 -0
- package/app/pages/workshop/agents.vue +92 -2
- package/app/pages/workshop/channel-templates.vue +53 -0
- package/app/pages/workshop/index.vue +14 -0
- package/app/pages/workshop/teams.vue +66 -6
- package/app/pages/workshop/w/[wsId].vue +202 -14
- package/app/plugins/aw-plugins.client.ts +13 -8
- package/app/stores/app.ts +33 -1
- package/app/stores/workshop/user.ts +1 -1
- package/app/stores/workshop/workspaces.ts +18 -2
- package/app/utils/api-error.ts +1 -1
- package/cli/commands/build.mjs +8 -1
- package/docker-compose.yml +6 -0
- package/i18n/locales/en.ts +2 -0
- package/i18n/locales/zh-CN.ts +2 -0
- package/nuxt.config.ts +31 -0
- package/package.json +9 -1
- package/scripts/_dbg-param-map-e2e.ts +242 -0
- package/scripts/_dbg-param-map-rest-smoke.mjs +73 -0
- package/scripts/_dbg-param-real-scenario.mjs +153 -0
- package/scripts/api-live-e2e.mjs +40 -8
- package/scripts/capture-walkthrough.mjs +232 -0
- package/scripts/render-walkthrough-figure.mjs +46 -0
- package/scripts/seed-walkthrough-fixture.mjs +143 -0
- package/scripts/test-plugin-hardening.mjs +8 -0
- package/scripts/ui/admin-shots.mjs +46 -0
- package/scripts/ui/audit-layout.mjs +301 -0
- package/scripts/ui/bump-microfont.mjs +39 -0
- package/scripts/ui/lib.mjs +198 -0
- package/scripts/ui/linkcheck2.mjs +37 -0
- package/scripts/ui/live-render-check.mjs +72 -0
- package/scripts/ui/md-lint.mjs +80 -0
- package/scripts/ui/perf-audit.mjs +132 -0
- package/scripts/ui/readme-assets.mjs +30 -0
- package/scripts/ui/readme-shots.mjs +42 -0
- package/scripts/ui/record.mjs +225 -0
- package/scripts/ui/seed-admin.mjs +68 -0
- package/scripts/ui/seed-demo.mjs +83 -0
- package/scripts/ui/shot-hero.mjs +25 -0
- package/scripts/ui/shots.mjs +75 -0
- package/scripts/ui/sim-ui-shot.mjs +16 -0
- package/scripts/ui/verify-nav.mjs +51 -0
- package/scripts/ui/verify-responsive.mjs +285 -0
- package/server/api/system/config-groups/[id].patch.ts +1 -1
- package/server/api/system/config-groups/index.post.ts +1 -1
- package/server/api/system/settings/events.get.ts +1 -1
- package/server/api/system/settings/index.patch.ts +1 -1
- package/server/api/workshop/agent-tools/invoke.post.ts +16 -2
- package/server/api/workshop/aml/datasets/[id].get.ts +8 -1
- package/server/api/workshop/daq/infra/reconnect.post.ts +1 -1
- package/server/api/workshop/daq/templates/[key].delete.ts +1 -0
- package/server/api/workshop/daq/templates/[key].patch.ts +1 -0
- package/server/api/workshop/daq/templates/index.post.ts +1 -0
- package/server/api/workshop/dcw/[id]/write.post.ts +10 -2
- package/server/api/workshop/dcw/[id].delete.ts +5 -2
- package/server/api/workshop/dcw/index.get.ts +2 -0
- package/server/api/workshop/dcw/lines/[id].patch.ts +1 -1
- package/server/api/workshop/dcw/params/[id]/read.post.ts +17 -0
- package/server/api/workshop/dcw/params/[id]/write.post.ts +21 -0
- package/server/api/workshop/dcw/params/[id].delete.ts +19 -0
- package/server/api/workshop/dcw/params/[id].patch.ts +23 -0
- package/server/api/workshop/dcw/params/index.get.ts +27 -0
- package/server/api/workshop/dcw/params/index.post.ts +25 -0
- package/server/api/workshop/permissions/index.get.ts +3 -1
- package/server/plugins/aw-plugins.ts +14 -3
- package/server/plugins/dev-stability-guard.ts +3 -1
- package/server/plugins/system-config.ts +6 -1
- package/server/services/system-config.ts +28 -9
- package/server/services/workshop/agents/adapters/one-shot-cli-agent.ts +2 -1
- package/server/services/workshop/agents/agent-interface.ts +8 -6
- package/server/services/workshop/agents/base-agent.ts +18 -4
- package/server/services/workshop/agents/claude-agent.ts +7 -3
- package/server/services/workshop/agents/codex-agent.ts +3 -3
- package/server/services/workshop/agents/dsh-agent.ts +3 -3
- package/server/services/workshop/agents/goose-agent.ts +0 -1
- package/server/services/workshop/agents/harness-terminal.ts +14 -1
- package/server/services/workshop/agents/hermes-agent.ts +4 -2
- package/server/services/workshop/agents/host-tool-bridge.ts +5 -1
- package/server/services/workshop/agents/industrial-context.ts +4 -1
- package/server/services/workshop/agents/industrial-tools.ts +164 -6
- package/server/services/workshop/agents/omp-agent.ts +15 -9
- package/server/services/workshop/agents/opencode-agent.ts +47 -21
- package/server/services/workshop/agents/qwen-agent.ts +10 -5
- package/server/services/workshop/aml/entity.ts +3 -1
- package/server/services/workshop/aml/python-runtime.ts +3 -2
- package/server/services/workshop/daq/bus/mqtt.adapter.ts +8 -4
- package/server/services/workshop/daq/daq-controller.ts +18 -3
- package/server/services/workshop/daq/daq-runtime.ts +5 -3
- package/server/services/workshop/daq/drivers.ts +63 -8
- package/server/services/workshop/daq/host-ports.ts +3 -2
- package/server/services/workshop/daq/storage/timescale.adapter.ts +3 -1
- package/server/services/workshop/db/channel.repo.ts +4 -0
- package/server/services/workshop/db/ops.repo.ts +5 -2
- package/server/services/workshop/db/retention.ts +8 -3
- package/server/services/workshop/dcw/dcw-controller.ts +143 -16
- package/server/services/workshop/dcw/dcw-product.repo.ts +38 -1
- package/server/services/workshop/dcw/dcw-runtime.ts +3 -2
- package/server/services/workshop/dcw/drivers.ts +5 -2
- package/server/services/workshop/dcw/param-conversion.ts +78 -0
- package/server/services/workshop/dcw/param-limits.ts +142 -0
- package/server/services/workshop/dcw/param-map.repo.ts +305 -0
- package/server/services/workshop/dcw/recipe-rollback-manager.ts +13 -7
- package/server/services/workshop/runtime/agent-runtime.ts +4 -0
- package/server/services/workshop/runtime/manager.ts +9 -2
- package/server/services/workshop/settings.ts +4 -2
- package/server/types/pg.d.ts +49 -0
- package/server/utils/stability-severity.ts +13 -2
- package/shared/config/groups.mjs +22 -1
- package/shared/config/home.d.mts +28 -0
- package/shared/dcw-protocol.ts +136 -0
- package/uno.config.ts +12 -1
- package/.output/public/_nuxt/BFkgkW8u.js +0 -4
- package/.output/public/_nuxt/C5gUkRwn.js +0 -1
- package/.output/public/_nuxt/CM12FuB42.js +0 -1
- package/.output/public/_nuxt/CZiw7sEV.js +0 -1
- package/.output/public/_nuxt/Cc_9VzD42.js +0 -1
- package/.output/public/_nuxt/CdPzkOMV.js +0 -1
- package/.output/public/_nuxt/CmsOtOAA2.js +0 -1
- package/.output/public/_nuxt/D1YDxZpZ.js +0 -1
- package/.output/public/_nuxt/D20Yamr4.js +0 -1
- package/.output/public/_nuxt/DmqknNvV.js +0 -18
- package/.output/public/_nuxt/DnUuIR83.js +0 -1
- package/.output/public/_nuxt/S_fVQwJK.js +0 -1
- package/.output/public/_nuxt/TownView.0oLiQgTC.css +0 -1
- package/.output/public/_nuxt/_id_.D-SYuyZb.css +0 -1
- package/.output/public/_nuxt/_id_.hma_BU6T.css +0 -1
- package/.output/public/_nuxt/_wsId_.B12v3PhY.css +0 -1
- package/.output/public/_nuxt/agents.BV70ioCv.css +0 -1
- package/.output/public/_nuxt/ahgEJu2R.js +0 -1
- package/.output/public/_nuxt/aml.COEKNNqK.css +0 -1
- package/.output/public/_nuxt/builds/meta/4457d63f-0866-4cea-b320-04a321f24795.json +0 -1
- package/.output/public/_nuxt/channel-templates.C-M5nKcC.css +0 -1
- package/.output/public/_nuxt/daq.B3265ZDn.css +0 -1
- package/.output/public/_nuxt/dcw.DLYZqEhT.css +0 -1
- package/.output/public/_nuxt/default.BXI_fZUi.css +0 -1
- package/.output/public/_nuxt/entry.BB-Eeh-J.css +0 -1
- package/.output/public/_nuxt/logs.CqFyMrJw.css +0 -1
- package/.output/public/_nuxt/monitor.B--1dPzs.css +0 -1
- package/.output/public/_nuxt/pages.ooEzrMm9.css +0 -1
- package/.output/public/_nuxt/permissions.B1pjGChZ.css +0 -1
- package/.output/public/_nuxt/plugins.5e5chZ67.css +0 -1
- package/.output/public/_nuxt/pz2JDAM7.js +0 -2
- package/.output/public/_nuxt/settings.Dm3M0RLy.css +0 -1
- package/.output/public/_nuxt/teams.BC75EGnR.css +0 -1
- package/.output/public/_nuxt/tokens.xRb1fSPU.css +0 -1
- package/.output/public/_nuxt/users.CXnP2E08.css +0 -1
- package/.output/public/_nuxt/workshop.Jk5HxWPE.css +0 -1
- package/.output/server/chunks/build/TownView-jVazYOjN.mjs.map +0 -1
- package/.output/server/chunks/build/_id_-Hwr4rZpr.mjs.map +0 -1
- package/.output/server/chunks/build/agents-Bvin73QY.mjs.map +0 -1
- package/.output/server/chunks/build/daq-C03Uu5UE.mjs.map +0 -1
- package/.output/server/chunks/build/pages-BO7yZsXA.mjs.map +0 -1
- package/.output/server/chunks/build/settings-DoQ2Oq3P.mjs.map +0 -1
- package/.output/server/chunks/build/teams-2kz3CusY.mjs.map +0 -1
- package/.output/server/chunks/build/town-BOl6hs9B.mjs.map +0 -1
- package/.output/server/chunks/build/workspaces-BuydxM-3.mjs.map +0 -1
- /package/.output/public/_nuxt/{36oeS8_R2.js → 36oeS8_R.js} +0 -0
- /package/.output/public/_nuxt/{CYWVXZTS.js → CYWVXZTS2.js} +0 -0
|
@@ -200,8 +200,17 @@ function mockImageSample(
|
|
|
200
200
|
// Modbus TCP 真实驱动(modbus-serial;连接池按 host:port:unitId 复用)
|
|
201
201
|
// ============================================================
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* modbus-serial 客户端实例类型。
|
|
205
|
+
*
|
|
206
|
+
* 该包 index.d.ts 只写 `import { ModbusRTU } from "./ModbusRTU"` + `export default ModbusRTU`,
|
|
207
|
+
* **没有**具名 re-export(运行时 module.exports = 构造器,并自挂 module.exports.default = 自己),
|
|
208
|
+
* 因此 `import('modbus-serial').ModbusRTU` 在类型上并不存在 —— 从 default 导出取才与实际导出形态一致。
|
|
209
|
+
*/
|
|
210
|
+
type ModbusRtuClient = InstanceType<typeof import('modbus-serial').default>
|
|
211
|
+
|
|
203
212
|
export interface ModbusConn {
|
|
204
|
-
client:
|
|
213
|
+
client: ModbusRtuClient
|
|
205
214
|
lastUsed: number
|
|
206
215
|
/** 连接级操作队列尾(采/控共用链路串行化:TCP 网关并发事务会协议错乱) */
|
|
207
216
|
tail: Promise<unknown>
|
|
@@ -232,7 +241,7 @@ export function evictModbusConn(cfg: Record<string, unknown>, transport: ModbusT
|
|
|
232
241
|
* 安全关闭:半开 socket(对端断电)时 modbus-serial close() 等待 FIN 永不返回,
|
|
233
242
|
* 一律 500ms 超时保护(fire-and-forget 底层 close,调用方不阻塞)。
|
|
234
243
|
*/
|
|
235
|
-
export async function closeModbusSafely(client:
|
|
244
|
+
export async function closeModbusSafely(client: ModbusRtuClient): Promise<void> {
|
|
236
245
|
await Promise.race([
|
|
237
246
|
Promise.resolve()
|
|
238
247
|
.then(() => client.close())
|
|
@@ -252,7 +261,7 @@ export function withModbusConn<R>(conn: ModbusConn, fn: () => Promise<R>): Promi
|
|
|
252
261
|
const run = conn.tail.then(() => fn(), () => fn())
|
|
253
262
|
let timer: NodeJS.Timeout | undefined
|
|
254
263
|
let dead = false
|
|
255
|
-
const guarded = Promise.race([
|
|
264
|
+
const guarded: Promise<R> = Promise.race([
|
|
256
265
|
run,
|
|
257
266
|
new Promise<never>((_, rej) => {
|
|
258
267
|
timer = setTimeout(() => {
|
|
@@ -264,7 +273,7 @@ export function withModbusConn<R>(conn: ModbusConn, fn: () => Promise<R>): Promi
|
|
|
264
273
|
rej(new Error('Modbus 响应超时(3s)——连接可能半开,已作废重连'))
|
|
265
274
|
}, 3000)
|
|
266
275
|
}),
|
|
267
|
-
]
|
|
276
|
+
])
|
|
268
277
|
void guarded.catch(() => { /* 超时分支先 reject,防 unhandled */ })
|
|
269
278
|
conn.tail = guarded.then(
|
|
270
279
|
() => {
|
|
@@ -298,7 +307,7 @@ export async function getModbusConn(cfg: Record<string, unknown>, transport: Mod
|
|
|
298
307
|
}
|
|
299
308
|
// CJS 互操作:require 形状可能是 { ModbusRTU } / default.ModbusRTU / 构造器本身
|
|
300
309
|
const mod = reqNative('modbus-serial') as unknown as { ModbusRTU?: unknown, default?: { ModbusRTU?: unknown } | unknown }
|
|
301
|
-
const ModbusRTU = (mod.ModbusRTU ?? (mod.default as { ModbusRTU?: unknown } | undefined)?.ModbusRTU ?? mod) as new () =>
|
|
310
|
+
const ModbusRTU = (mod.ModbusRTU ?? (mod.default as { ModbusRTU?: unknown } | undefined)?.ModbusRTU ?? mod) as new () => ModbusRtuClient
|
|
302
311
|
const client = new ModbusRTU()
|
|
303
312
|
client.setTimeout(3000)
|
|
304
313
|
// tcp = Modbus TCP(MBAP 封装);rtu-tcp = RTU over TCP(串口网关透传,CRC16 帧,无 MBAP)
|
|
@@ -490,6 +499,17 @@ export interface OpcUaConn {
|
|
|
490
499
|
|
|
491
500
|
const opcuaPool = new Map<string, OpcUaConn>()
|
|
492
501
|
|
|
502
|
+
/**
|
|
503
|
+
* 在飞建连去重表(同池 key → 尚未 settle 的建连任务)。
|
|
504
|
+
*
|
|
505
|
+
* 修复动因(实测事故):同端点**并发首连**此前无去重——两条采样/写控请求同时进入时
|
|
506
|
+
* 各建一条 OPCUAClient,而后一条的 `opcuaPool.set` 会**覆盖**前一条:
|
|
507
|
+
* 被覆盖的 session 与 TCP 连接从此无人持有,空闲 sweep 也扫不到 → **永久泄漏**;
|
|
508
|
+
* 重连 churn(断链演练/反复建节点)下还会触发 node-opcua 内部
|
|
509
|
+
* `_internal_create_secure_channel failed, this._secureChannel is supposed to be null`。
|
|
510
|
+
*/
|
|
511
|
+
const opcuaInflight = new Map<string, Promise<OpcUaConn>>()
|
|
512
|
+
|
|
493
513
|
// OPC UA 会话空闲回收(对照 modbus 池 sweep;节点删除/端点弃用后不再有读取
|
|
494
514
|
// 路径触发 3 错误驱逐,无 sweep 会话与 TCP 连接会永久驻留)
|
|
495
515
|
const OPCUA_IDLE_MS = 600_000
|
|
@@ -533,6 +553,20 @@ export async function getOpcUaConn(cfg: Record<string, unknown>): Promise<OpcUaC
|
|
|
533
553
|
existing.lastUsed = Date.now()
|
|
534
554
|
return existing
|
|
535
555
|
}
|
|
556
|
+
// 在飞去重:同 key 并发首连复用同一条建连任务(否则后写覆盖 pool → 前一条 session/socket 永久泄漏)
|
|
557
|
+
const pending = opcuaInflight.get(key)
|
|
558
|
+
if (pending) return pending
|
|
559
|
+
const task = createOpcUaConn(cfg, key)
|
|
560
|
+
opcuaInflight.set(key, task)
|
|
561
|
+
try {
|
|
562
|
+
return await task
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
opcuaInflight.delete(key)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function createOpcUaConn(cfg: Record<string, unknown>, key: string): Promise<OpcUaConn> {
|
|
536
570
|
const opcua = reqNative('node-opcua') as typeof import('node-opcua')
|
|
537
571
|
const securityMode = (['None', 'Sign', 'SignAndEncrypt'] as const).includes(cfg.securityMode as 'None')
|
|
538
572
|
? (cfg.securityMode as 'None')
|
|
@@ -574,15 +608,33 @@ export async function getOpcUaConn(cfg: Record<string, unknown>): Promise<OpcUaC
|
|
|
574
608
|
}
|
|
575
609
|
const client = opcua.OPCUAClient.create(clientOpts as Parameters<typeof opcua.OPCUAClient.create>[0])
|
|
576
610
|
await client.connect(String(cfg.endpoint))
|
|
611
|
+
// type 是 node-opcua 的 UserIdentityInfo 判别式(node-opcua-client createUserIdentityToken →
|
|
612
|
+
// coerceUserIdentityInfo:userName 存在时正是补成 UserTokenType.UserName 走同一分支),
|
|
613
|
+
// 显式写出既满足协议类型,也免去"字段漏写、等库回填"的隐式依赖;两个分支都返回 Promise<ClientSession>。
|
|
577
614
|
const session = cfg.username
|
|
578
|
-
? await client.createSession({ userName: String(cfg.username), password: String(cfg.password ?? '') })
|
|
615
|
+
? await client.createSession({ type: opcua.UserTokenType.UserName, userName: String(cfg.username), password: String(cfg.password ?? '') })
|
|
579
616
|
: await client.createSession()
|
|
580
617
|
const conn: OpcUaConn = { session, client, lastUsed: Date.now(), errors: 0 }
|
|
618
|
+
// 竞态二次校验:建连期间若已有连接入池(并发首连的另一方完成),关闭本次新建并复用既有,
|
|
619
|
+
// 避免 opcuaPool.set 覆盖导致 session/socket 永久泄漏。
|
|
620
|
+
const raced = opcuaPool.get(key)
|
|
621
|
+
if (raced) {
|
|
622
|
+
try {
|
|
623
|
+
await session.close()
|
|
624
|
+
}
|
|
625
|
+
catch { /* 清理失败不覆盖原始错误 */ }
|
|
626
|
+
try {
|
|
627
|
+
await client.disconnect()
|
|
628
|
+
}
|
|
629
|
+
catch { /* 清理失败不覆盖原始错误 */ }
|
|
630
|
+
return raced
|
|
631
|
+
}
|
|
581
632
|
opcuaPool.set(key, conn)
|
|
582
633
|
return conn
|
|
583
634
|
}
|
|
584
635
|
|
|
585
|
-
/** 驱逐 OPC UA 连接(采样/写控共享池;连续故障后触发,下次调用重建)
|
|
636
|
+
/** 驱逐 OPC UA 连接(采样/写控共享池;连续故障后触发,下次调用重建)。
|
|
637
|
+
* 只作用于**已入池**的连接:在飞建连本身就是新建的,交由竞态二次校验处理。 */
|
|
586
638
|
export async function evictOpcUaConn(cfg: Record<string, unknown>): Promise<void> {
|
|
587
639
|
const key = opcuaKey(cfg)
|
|
588
640
|
const conn = opcuaPool.get(key)
|
|
@@ -961,7 +1013,10 @@ export const httpDaqDriver: DaqDriver = {
|
|
|
961
1013
|
return { ok: true, message: `接口可达,vector 帧 ${(j.points as unknown[]).length} 点`, latencyMs: Date.now() - t0 }
|
|
962
1014
|
}
|
|
963
1015
|
if (typeof j.png === 'string' || typeof j.blob === 'string') {
|
|
964
|
-
|
|
1016
|
+
// 与生产分支同构:png/blob 至少一个是 base64 字符串(见上一行守卫)。
|
|
1017
|
+
// b64 的取值表达式与原来逐字一致(断言运行时擦除),只是把 unknown 标注成 string 以取 .length
|
|
1018
|
+
const b64 = (j.png as string | undefined) ?? (j.blob as string)
|
|
1019
|
+
return { ok: true, message: `接口可达,image 帧(base64, ${b64.length} chars)`, latencyMs: Date.now() - t0 }
|
|
965
1020
|
}
|
|
966
1021
|
const v = extractNumeric(text, driverConfig.jsonPath ? String(driverConfig.jsonPath) : undefined)
|
|
967
1022
|
return { ok: true, message: `接口可达,取值 = ${v}`, sampleValue: v, latencyMs: Date.now() - t0 }
|
|
@@ -20,10 +20,11 @@ export interface ActiveRunSnapshot {
|
|
|
20
20
|
/** 遥测上报端口:绑定设备的值回写 + 孪生场景推送 + 绑定校验(中心实现 = device-twin.repo) */
|
|
21
21
|
export interface DaqTelemetryPort {
|
|
22
22
|
/**
|
|
23
|
-
* 遥测回写:telemetry 键值写入绑定设备的孪生,state
|
|
23
|
+
* 遥测回写:telemetry 键值写入绑定设备的孪生,state 为同设备多节点汇聚的最严重态
|
|
24
|
+
* (alarm > warn > ok,与 device-twin.repo.applyTelemetry 的 nodeState 同一个域)。
|
|
24
25
|
* 目标设备不存在 → { ok:false }(调用方解绑自身,链路自愈);成功 → { ok:true, twinId }。
|
|
25
26
|
*/
|
|
26
|
-
applyTelemetry(deviceId: string, telemetry: Record<string, number | string | boolean>, state:
|
|
27
|
+
applyTelemetry(deviceId: string, telemetry: Record<string, number | string | boolean>, state: 'alarm' | 'warn' | 'ok'): { ok: boolean, twinId: string | null }
|
|
27
28
|
/** 孪生场景载荷(WS device.updated 帧;设备已删 → null) */
|
|
28
29
|
scenePayload(twinId: string): unknown | null
|
|
29
30
|
/** 绑定目标存在性校验 */
|
|
@@ -239,7 +239,9 @@ export class TimescaleAdapter implements TsdbPort {
|
|
|
239
239
|
at: tsToMs(r.ts),
|
|
240
240
|
kind: String(r.kind) as 'vector' | 'image',
|
|
241
241
|
points: String(r.kind) === 'vector' ? pointsFromMeta(r.meta) : undefined,
|
|
242
|
-
metrics
|
|
242
|
+
// 与 sqlite.adapter 同法:metrics 列由本管线写入(下沉处理器产出 avg/max/brightness/zone_* 数值),
|
|
243
|
+
// asObject 只做 JSON 还原(Record<string, unknown>),按端口契约收窄到 Record<string, number>(断言运行时擦除)
|
|
244
|
+
metrics: asObject(r.metrics) as Record<string, number>,
|
|
243
245
|
meta: asObject(r.meta),
|
|
244
246
|
deviceBindingId: r.device_binding_id ?? null,
|
|
245
247
|
lineId: r.line_id ?? null,
|
|
@@ -54,6 +54,10 @@ export function createChannelRepo(db: DatabaseSync) {
|
|
|
54
54
|
name: input.name,
|
|
55
55
|
description: input.description ?? '',
|
|
56
56
|
scenarioPrompt: input.scenarioPrompt ?? '',
|
|
57
|
+
// INSERT 不含该列 → 落库取列默认(llm_json TEXT NOT NULL DEFAULT '',见 database.ts v11);
|
|
58
|
+
// 返回的内存行按同一事实补该列,使 create() 的返回值与随后 findById() 读到的行形状一致。
|
|
59
|
+
// 注意:insert.run(...) 的参数列表保持不变(多传一个参数会与 SQL 占位符数量不符)。
|
|
60
|
+
llmJson: '',
|
|
57
61
|
leadAgentId: null,
|
|
58
62
|
workspace: input.workspace ?? '',
|
|
59
63
|
enabled: 1,
|
|
@@ -104,9 +104,12 @@ export function createAlarmEventRepo(db: DatabaseSync) {
|
|
|
104
104
|
|
|
105
105
|
return {
|
|
106
106
|
/** 按 id 取报警关联节点(产线权限判定用);不存在返回 undefined */
|
|
107
|
-
nodeIdById(id) {
|
|
107
|
+
nodeIdById(id: string): string | undefined {
|
|
108
108
|
const r = db.prepare('SELECT node_id FROM alarm_events WHERE id = ?').get(id)
|
|
109
|
-
|
|
109
|
+
// node_id 列是 TEXT NOT NULL,写入侧恒为 string(见 database.ts 的 alarm_events 定义);
|
|
110
|
+
// 显式收窄后,调用方拿到的是 string|undefined,而不是 node:sqlite 的 SQLOutputValue 联合
|
|
111
|
+
const v = r?.node_id
|
|
112
|
+
return typeof v === 'string' ? v : undefined
|
|
110
113
|
},
|
|
111
114
|
|
|
112
115
|
/** 报警产生(同 node+metric 未确认报警幂等去重,防止高频越限刷表) */
|
|
@@ -9,11 +9,16 @@
|
|
|
9
9
|
* 覆盖链:config.yml < runtime-settings < env(AW_<键> 或历史别名)。时间列均为 TEXT ISO。
|
|
10
10
|
* channel_events 与 daq_samples 已有各自保留机制,此处不重复覆盖。
|
|
11
11
|
*/
|
|
12
|
+
import type { DatabaseSync } from 'node:sqlite'
|
|
12
13
|
import { retentionSettings } from '../settings'
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 库句柄契约 = node:sqlite DatabaseSync 的只读子集。
|
|
17
|
+
* 早先手写的鸭子类型把 run 声明为 `(...args: unknown[])`,与 StatementSync 的
|
|
18
|
+
* `...anonymousParameters: SQLInputValue[]` 反向不兼容(unknown 形参不接受 SQLInputValue),
|
|
19
|
+
* 于是**真实的 DatabaseSync 反而传不进来**。直接从权威类型取子集即消除该漂移(纯类型,运行时零变化)。
|
|
20
|
+
*/
|
|
21
|
+
type DatabaseSyncLike = Pick<DatabaseSync, 'prepare'>
|
|
17
22
|
|
|
18
23
|
const DAY_MS = 86_400_000
|
|
19
24
|
const BATCH = 5000
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { randomUUID } from 'node:crypto'
|
|
13
13
|
import { applyTransform, inverseTransform, normalizeDataTransform, dcwKeyFromRef } from '../../../../shared/dcw-protocol'
|
|
14
|
-
import type { AepDcwNodeChange, DcwDriverKind, DcwNodeView, DataTransform, LineInput, LineQueryOpts, LineQueryResult, LineRunState, LineView, ProductInput, ProductView, RecipeInput, RecipeRunView, RecipeView, DcwWriteMeta } from '../../../../shared/dcw-protocol'
|
|
14
|
+
import type { AepDcwNodeChange, DcwDriverKind, DcwNodeView, DcwParamView, DataTransform, LineInput, LineQueryOpts, LineQueryResult, LineRunState, LineView, ProductInput, ProductView, RecipeInput, RecipeRunView, RecipeView, DcwWriteMeta } from '../../../../shared/dcw-protocol'
|
|
15
15
|
import { AppError, ErrorCodes } from '../../../utils/errors'
|
|
16
16
|
import { normalizeDcwDriverKind, resolveDcwDriver } from './drivers'
|
|
17
17
|
import { findDcwTemplate } from './dcw-templates'
|
|
@@ -22,6 +22,9 @@ import { DcwNodeRuntime } from './dcw-runtime'
|
|
|
22
22
|
import { getDcwRecipeRepo, type DcwWriteHistoryEntry } from './dcw-recipe.repo'
|
|
23
23
|
import { getDcwProductRepo } from './dcw-product.repo'
|
|
24
24
|
import { getDcwLineRepo } from './dcw-line.repo'
|
|
25
|
+
import { getDcwParamRepo } from './param-map.repo'
|
|
26
|
+
import { assertWithinLimits, limitsBreakdownOf } from './param-limits'
|
|
27
|
+
import { deriveAccess } from './param-conversion'
|
|
25
28
|
import { emitDcwWrite } from '@/server/services/workshop/plugins/host.mjs'
|
|
26
29
|
import { clearActiveLineRun, getActiveLineRun, getAllActiveLineRuns, setActiveLineRun } from './line-run'
|
|
27
30
|
import { getRecipeRollBackManager } from './recipe-rollback-manager'
|
|
@@ -353,6 +356,9 @@ class DcwController {
|
|
|
353
356
|
semantics: input.semantics,
|
|
354
357
|
})
|
|
355
358
|
this.repo.insert(node)
|
|
359
|
+
// 工艺参数映射面:节点创建即自动生成同名映射(用户/Agent 的参数语义面;
|
|
360
|
+
// PLC 寻址细节仍封装在节点驱动配置内)。失败不阻断节点创建(可事后手工补建)。
|
|
361
|
+
getDcwParamRepo().ensureForNode(node)
|
|
356
362
|
this.syncRuntimes()
|
|
357
363
|
this.emitNodeChanged('added', node)
|
|
358
364
|
return node
|
|
@@ -431,6 +437,8 @@ class DcwController {
|
|
|
431
437
|
if (!node) throw new AppError(404, ErrorCodes.NOT_FOUND, `控制节点不存在: ${id}`)
|
|
432
438
|
this.repo.remove(id)
|
|
433
439
|
this.runtimes.delete(id)
|
|
440
|
+
// 级联清理:工艺参数映射面随执行节点删除(参数不存在悬空引用)
|
|
441
|
+
getDcwParamRepo().removeForNode(id)
|
|
434
442
|
// 级联清理:该节点的 Agent 绑定移除,挂起中的手动审批按失效收敛
|
|
435
443
|
void import('../agents/node-bindings.repo').then(({ getAgentNodeBindingRepo }) => {
|
|
436
444
|
const repo = getAgentNodeBindingRepo()
|
|
@@ -537,22 +545,21 @@ class DcwController {
|
|
|
537
545
|
}
|
|
538
546
|
// 调控闭环护栏(F1/F8):Agent 互斥(open 记录他人持有)+ 回退冷却方向性
|
|
539
547
|
getRecipeRollBackManager().beforeWrite(node, eng, meta)
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
548
|
+
// 写入限界分层联锁(param-limits):节点安全量程(结构层,驱动内 validateEng 结构性兜底)
|
|
549
|
+
// ∩ 工艺参数基准限界 ∩ 活动产品限界 ∩ 活动配方工艺窗口 —— 逐层收窄取交集,
|
|
550
|
+
// 手动 REST / Agent 工具 / 配方下发 / 回退四路共用本咽喉点,越界一律拒绝。
|
|
551
|
+
// D8 基准消融:仅 AW_BENCH_MODE=1 且 meta.benchArm='no-interlock'/'ungated' 时
|
|
552
|
+
// 旁路软联锁层(参数/产品/配方);结构层与硬量程校验结构性不可旁路;
|
|
553
|
+
// 生产模式(env 缺省)恒为 full 臂。
|
|
554
|
+
const benchNoInterlock = process.env.AW_BENCH_MODE === '1' && (meta?.benchArm === 'no-interlock' || meta?.benchArm === 'ungated')
|
|
555
|
+
// 配方下发路径(recipeRunId != null)跳过配方窗口层:下发值已在配方保存时对自身窗口校验,
|
|
556
|
+
// 且中途应用新配方不应被旧批次配方窗口误伤;产品/参数基准层对配方下发照常约束。
|
|
557
|
+
assertWithinLimits(node, eng, { includeSoft: !benchNoInterlock, skipRecipe: recipeRunId != null })
|
|
558
|
+
// D8: no-readback / ungated 臂 → 容差置 MAX,回读差异不致败(假成功语义,供 I2 消融)
|
|
559
|
+
const benchNoReadback = process.env.AW_BENCH_MODE === '1' && (meta?.benchArm === 'no-readback' || meta?.benchArm === 'ungated')
|
|
560
|
+
const benchToleranceOverride = benchNoReadback ? Number.MAX_VALUE : undefined
|
|
554
561
|
const prevValue = typeof node.value === 'number' ? node.value : null
|
|
555
|
-
const outcome = await rt.write(eng, recipeRunId)
|
|
562
|
+
const outcome = await rt.write(eng, recipeRunId, benchToleranceOverride)
|
|
556
563
|
// 调控闭环入册:仅成功写记锚/开记录(失败写不动账本 —— PLC 值未变更);
|
|
557
564
|
// 窗口聚合异步回填(F2)
|
|
558
565
|
if (outcome.ok !== false) {
|
|
@@ -618,6 +625,90 @@ class DcwController {
|
|
|
618
625
|
return this.testDriver(node.driver, node.driverConfig)
|
|
619
626
|
}
|
|
620
627
|
|
|
628
|
+
// ---------- 工艺参数映射面(用户/Agent 的唯一语义读写面)----------
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 创建工艺参数映射。两条路径:
|
|
632
|
+
* - nodeId 绑定路径:映射指向既有执行节点;
|
|
633
|
+
* - access 接入路径:设备连接 + 寄存器 + **标准转换模式**一键建映射 —— 系统
|
|
634
|
+
* 按转换模式展开驱动配置并自动创建执行节点(float32 直写 / int16·int32 线性
|
|
635
|
+
* 标定),配置期一次性选定,运行期双向换算全自动。
|
|
636
|
+
*/
|
|
637
|
+
createParamMapping(input: DcwParamInput): DcwParamView {
|
|
638
|
+
const access = input.access
|
|
639
|
+
if (!access) {
|
|
640
|
+
const row = getDcwParamRepo().create(input)
|
|
641
|
+
return this.paramById(row.id)
|
|
642
|
+
}
|
|
643
|
+
if (input.nodeId) {
|
|
644
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'access 与 nodeId 互斥:access 路径由系统自动创建执行节点')
|
|
645
|
+
}
|
|
646
|
+
if (!input.templateRef) {
|
|
647
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'templateRef 必填:工艺参数需绑定语义模板(单位/量程/语义)')
|
|
648
|
+
}
|
|
649
|
+
const lineId = String(input.lineId ?? '')
|
|
650
|
+
if (lineId && !getDcwLineRepo().byId(lineId)) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lineId}`)
|
|
651
|
+
const derived = deriveAccess(access)
|
|
652
|
+
// 执行节点:驱动配置由转换模式展开(单一事实源);标定工程量程即节点安全量程
|
|
653
|
+
const node = this.create({
|
|
654
|
+
templateRef: input.templateRef,
|
|
655
|
+
name: input.name,
|
|
656
|
+
driver: derived.driver,
|
|
657
|
+
driverConfig: derived.driverConfig,
|
|
658
|
+
min: derived.engMin,
|
|
659
|
+
max: derived.engMax,
|
|
660
|
+
lineId,
|
|
661
|
+
})
|
|
662
|
+
// this.create() 已自动生成参数面;覆盖语义面(key/名称/基准限界)并留存转换模式
|
|
663
|
+
const auto = getDcwParamRepo().byNode(node.id)
|
|
664
|
+
const row = getDcwParamRepo().update(auto!.id, {
|
|
665
|
+
key: input.key,
|
|
666
|
+
name: input.name,
|
|
667
|
+
min: input.min ?? null,
|
|
668
|
+
max: input.max ?? null,
|
|
669
|
+
conversion: access.conversion,
|
|
670
|
+
})
|
|
671
|
+
return this.paramById(row.id)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
listParamViews(): DcwParamView[] {
|
|
675
|
+
return getDcwParamRepo().listViews()
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
paramById(id: string): DcwParamView {
|
|
679
|
+
const row = getDcwParamRepo().byId(id)
|
|
680
|
+
if (!row) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在: ${id}`)
|
|
681
|
+
const view = getDcwParamRepo().viewOf(row)
|
|
682
|
+
if (!view) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数 ${id} 的执行节点已不存在(映射悬空)`)
|
|
683
|
+
return view
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** 参数寻址写命令:工程量直写,限界联锁在 write() 咽喉点统一生效 */
|
|
687
|
+
async writeParam(id: string, value: number, meta?: DcwWriteMeta, recipeRunId: string | null = null) {
|
|
688
|
+
const row = getDcwParamRepo().byId(id)
|
|
689
|
+
const node = row ? this.repo.byId(row.nodeId) : undefined
|
|
690
|
+
if (!row || !node) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在(或执行节点已删除): ${id}`)
|
|
691
|
+
const outcome = await this.write(node.id, value, recipeRunId, meta)
|
|
692
|
+
return { ...outcome, param: getDcwParamRepo().viewOf(row) }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** 参数寻址读命令(读执行节点 PLC 值,标定解码为物理量) */
|
|
696
|
+
async readParam(id: string) {
|
|
697
|
+
const row = getDcwParamRepo().byId(id)
|
|
698
|
+
const node = row ? this.repo.byId(row.nodeId) : undefined
|
|
699
|
+
if (!row || !node) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在(或执行节点已删除): ${id}`)
|
|
700
|
+
const read = await this.readNow(node.id)
|
|
701
|
+
return { ...read, param: getDcwParamRepo().viewOf(row) }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** 参数当前有效写入限界剖面(分层 + 交集;展示/工具回包共用) */
|
|
705
|
+
paramLimitsOf(id: string) {
|
|
706
|
+
const row = getDcwParamRepo().byId(id)
|
|
707
|
+
const node = row ? this.repo.byId(row.nodeId) : undefined
|
|
708
|
+
if (!row || !node) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在(或执行节点已删除): ${id}`)
|
|
709
|
+
return limitsBreakdownOf(node)
|
|
710
|
+
}
|
|
711
|
+
|
|
621
712
|
// ---------- Recipe 配方 ----------
|
|
622
713
|
|
|
623
714
|
listRecipes() {
|
|
@@ -950,6 +1041,7 @@ class DcwController {
|
|
|
950
1041
|
createProduct(input: ProductInput): ProductView {
|
|
951
1042
|
const lineId = String(input.lineId ?? '')
|
|
952
1043
|
if (lineId && !getDcwLineRepo().byId(lineId)) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lineId}`)
|
|
1044
|
+
assertProductParamLimits(lineId, input.paramLimits)
|
|
953
1045
|
return getDcwProductRepo().create({ ...input, lineId })
|
|
954
1046
|
}
|
|
955
1047
|
|
|
@@ -959,6 +1051,8 @@ class DcwController {
|
|
|
959
1051
|
if (!getDcwLineRepo().byId(lid)) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lid}`)
|
|
960
1052
|
}
|
|
961
1053
|
const prev = getDcwProductRepo().byId(id)
|
|
1054
|
+
const targetLine = String(patch.lineId ?? prev?.lineId ?? '')
|
|
1055
|
+
assertProductParamLimits(targetLine, patch.paramLimits)
|
|
962
1056
|
const updated = getDcwProductRepo().update(id, patch)
|
|
963
1057
|
// 产品换线 → 旗下配方产线归属级联(开跑校验按 recipe.lineId)
|
|
964
1058
|
if (prev && prev.lineId !== updated.lineId) {
|
|
@@ -1032,6 +1126,39 @@ class DcwController {
|
|
|
1032
1126
|
|
|
1033
1127
|
// ---------- 单例(HMR 存活) ----------
|
|
1034
1128
|
|
|
1129
|
+
/**
|
|
1130
|
+
* 产品级工艺参数限界校验:键必须命中该产品所属产线上的工艺参数映射
|
|
1131
|
+
* (防静默失效的拼错键);区间须 min<=max 且为有限数字。产品未挂产线时限界无
|
|
1132
|
+
* 寻址语境,拒绝非空限界。
|
|
1133
|
+
*/
|
|
1134
|
+
function assertProductParamLimits(lineId: string, paramLimits: ProductInput['paramLimits'] | undefined): void {
|
|
1135
|
+
if (paramLimits == null) return
|
|
1136
|
+
if (typeof paramLimits !== 'object' || Array.isArray(paramLimits)) {
|
|
1137
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'paramLimits 需为对象(键=工艺参数 key,值={min?,max?})')
|
|
1138
|
+
}
|
|
1139
|
+
const entries = Object.entries(paramLimits)
|
|
1140
|
+
if (entries.length === 0) return
|
|
1141
|
+
if (!lineId) {
|
|
1142
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, '产品未挂载产线,不可设定工艺参数限界(限界按产线内的工艺参数寻址)')
|
|
1143
|
+
}
|
|
1144
|
+
const lineKeys = new Set(
|
|
1145
|
+
getDcwParamRepo().listViews().filter(p => p.lineId === lineId).map(p => p.key),
|
|
1146
|
+
)
|
|
1147
|
+
for (const [key, range] of entries) {
|
|
1148
|
+
if (!lineKeys.has(key)) {
|
|
1149
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `产品限界的工艺参数键「${key}」在产线上不存在(可用的工艺参数:${[...lineKeys].join(', ') || '无 —— 先创建写控节点生成参数面'})`)
|
|
1150
|
+
}
|
|
1151
|
+
const min = range?.min == null ? null : Number(range.min)
|
|
1152
|
+
const max = range?.max == null ? null : Number(range.max)
|
|
1153
|
+
if ((range?.min != null && !Number.isFinite(min)) || (range?.max != null && !Number.isFinite(max))) {
|
|
1154
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `产品限界「${key}」的上下限需为数字`)
|
|
1155
|
+
}
|
|
1156
|
+
if (min != null && max != null && min > max) {
|
|
1157
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `产品限界「${key}」非法:min ${min} > max ${max}`)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1035
1162
|
const g = globalThis as typeof globalThis & { __dcwController?: DcwController }
|
|
1036
1163
|
|
|
1037
1164
|
export function getDcwController(): DcwController {
|
|
@@ -19,13 +19,43 @@ const DB_PATH = join(ensureDataDir(), 'dcw-products.json')
|
|
|
19
19
|
function load(): ProductView[] {
|
|
20
20
|
try {
|
|
21
21
|
const parsed = loadJsonFile(DB_PATH, null)
|
|
22
|
-
return Array.isArray(parsed) ? parsed as ProductView[] : []
|
|
22
|
+
return Array.isArray(parsed) ? parsed.map(normRow) as ProductView[] : []
|
|
23
23
|
}
|
|
24
24
|
catch {
|
|
25
25
|
return []
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** 行规范化:paramLimits 键值容错(非有限数字的侧丢弃;min>max 整条丢弃) */
|
|
30
|
+
function normRow(p: ProductView): ProductView {
|
|
31
|
+
const raw = p.paramLimits
|
|
32
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) return p
|
|
33
|
+
const out: ProductView['paramLimits'] = {}
|
|
34
|
+
for (const [key, range] of Object.entries(raw)) {
|
|
35
|
+
if (!key || range == null || typeof range !== 'object') continue
|
|
36
|
+
const min = range.min == null ? undefined : Number(range.min)
|
|
37
|
+
const max = range.max == null ? undefined : Number(range.max)
|
|
38
|
+
const okMin = min == null || Number.isFinite(min)
|
|
39
|
+
const okMax = max == null || Number.isFinite(max)
|
|
40
|
+
if (!okMin || !okMax) continue
|
|
41
|
+
if (min != null && max != null && min > max) continue
|
|
42
|
+
const entry: { min?: number, max?: number } = {}
|
|
43
|
+
if (min != null) entry.min = min
|
|
44
|
+
if (max != null) entry.max = max
|
|
45
|
+
if (Object.keys(entry).length > 0) out[key] = entry
|
|
46
|
+
}
|
|
47
|
+
return { ...p, paramLimits: out }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 载荷规范化(创建/更新共用;键级有效性由控制器按产线工艺参数校验) */
|
|
51
|
+
function normLimitsInput(v: unknown): Record<string, { min?: number, max?: number }> | undefined {
|
|
52
|
+
if (v == null) return undefined
|
|
53
|
+
if (typeof v !== 'object' || Array.isArray(v)) {
|
|
54
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'paramLimits 需为对象(键=工艺参数 key)')
|
|
55
|
+
}
|
|
56
|
+
return normRow({ paramLimits: v as ProductView['paramLimits'] } as ProductView).paramLimits
|
|
57
|
+
}
|
|
58
|
+
|
|
29
59
|
class DcwProductRepo {
|
|
30
60
|
private list: ProductView[] = load()
|
|
31
61
|
|
|
@@ -47,6 +77,8 @@ class DcwProductRepo {
|
|
|
47
77
|
description: String(input.description ?? '').trim(),
|
|
48
78
|
createdAt: new Date().toISOString(),
|
|
49
79
|
}
|
|
80
|
+
const limits = normLimitsInput(input.paramLimits)
|
|
81
|
+
if (limits) p.paramLimits = limits
|
|
50
82
|
this.list.push(p)
|
|
51
83
|
this.flush()
|
|
52
84
|
return p
|
|
@@ -62,6 +94,11 @@ class DcwProductRepo {
|
|
|
62
94
|
}
|
|
63
95
|
if (patch.description !== undefined) p.description = String(patch.description).trim()
|
|
64
96
|
if (patch.lineId !== undefined) p.lineId = String(patch.lineId)
|
|
97
|
+
if (patch.paramLimits !== undefined) {
|
|
98
|
+
const limits = normLimitsInput(patch.paramLimits)
|
|
99
|
+
if (limits && Object.keys(limits).length > 0) p.paramLimits = limits
|
|
100
|
+
else delete p.paramLimits
|
|
101
|
+
}
|
|
65
102
|
this.flush()
|
|
66
103
|
return p
|
|
67
104
|
}
|
|
@@ -64,7 +64,7 @@ export class DcwNodeRuntime {
|
|
|
64
64
|
* 用户/Recipe 设定值(核心写命令入口)。
|
|
65
65
|
* 工艺安全量程硬校验(越界拒绝)+ 在飞互斥(同一通道同时只允许一个写事务)。
|
|
66
66
|
*/
|
|
67
|
-
async write(eng: number, recipeRunId: string | null = null): Promise<DcwWriteOutcome> {
|
|
67
|
+
async write(eng: number, recipeRunId: string | null = null, toleranceOverride?: number): Promise<DcwWriteOutcome> {
|
|
68
68
|
const node = this.node
|
|
69
69
|
const invalid = node.validateEng(eng)
|
|
70
70
|
if (invalid) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, invalid)
|
|
@@ -75,7 +75,8 @@ export class DcwNodeRuntime {
|
|
|
75
75
|
node.state = 'writing'
|
|
76
76
|
node.lastWriteAt = new Date().toISOString()
|
|
77
77
|
try {
|
|
78
|
-
|
|
78
|
+
// D8 基准消融:no-readback/ungated 臂由 write() 传入 MAX 容差 → 回读差异不再致败(假成功语义)
|
|
79
|
+
return await this.host.executeWrite(node, eng, toleranceOverride ?? writeTolerance(node), recipeRunId)
|
|
79
80
|
}
|
|
80
81
|
catch (err) {
|
|
81
82
|
// 执行链抛错(异常不流向 outcome):状态机必须离开 writing,否则通道永久 409
|
|
@@ -524,7 +524,10 @@ export const mqttDcwDriver: DcwWriteDriver = {
|
|
|
524
524
|
// 失败/超时路径同样关闭连接(此前只在成功后 end,超时会泄漏 client +
|
|
525
525
|
// broker session),已关闭的 client 重复 end 幂等安全
|
|
526
526
|
await new Promise<void>((resolve) => {
|
|
527
|
-
|
|
527
|
+
// mqtt 的 DoneCallback 是 Node 风格 (error?: Error) => void;直接把 Promise 的 resolve
|
|
528
|
+
// 传进去会把 error 当成"完成值"兑现(错误被吞)。这里显式包一层,保持原语义:
|
|
529
|
+
// 回调一触发即完成,且与原来一样**不**因该 error 而 reject。
|
|
530
|
+
client.end(false, {}, () => resolve())
|
|
528
531
|
})
|
|
529
532
|
}
|
|
530
533
|
return {
|
|
@@ -552,7 +555,7 @@ export const mqttDcwDriver: DcwWriteDriver = {
|
|
|
552
555
|
})
|
|
553
556
|
c.once('connect', () => resolve(c))
|
|
554
557
|
c.once('error', err => reject(new Error(err.message)))
|
|
555
|
-
}).then(c => new Promise<void>(r => c.end(false, {}, r)))
|
|
558
|
+
}).then(c => new Promise<void>(r => c.end(false, {}, () => r())))
|
|
556
559
|
return { ok: true, message: `Broker 连接成功(${String(driverConfig.host)}:${Number(driverConfig.port ?? 1883)});未执行发布,避免误触发真实设备动作` }
|
|
557
560
|
}
|
|
558
561
|
catch (err) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParamConversion —— 工艺参数 → PLC 标准转换模式展开器(配置期)。
|
|
3
|
+
*
|
|
4
|
+
* 用户在映射配置期选定「标准转换模式」(float32 直写 / int16·int32 线性标定)
|
|
5
|
+
* 与设备连接 + 寄存器,本模块将其一次性展开为执行节点驱动配置:
|
|
6
|
+
* - float32 原始值 = 工程值,驱动直写 float32
|
|
7
|
+
* - int16-scaled eng∈[engMin,engMax] 线性映射 raw∈[rawMin,rawMax],int16 编码
|
|
8
|
+
* - int32-scaled 同上,int32 编码
|
|
9
|
+
* 运行期双向换算(下发 encode / 回读 decode)由既有驱动链完成,本模块零参与 ——
|
|
10
|
+
* 展开产物即单一事实源(driverConfig),转换模式本身仅作展示/审计摘要留存。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ParamAccessSpec } from '../../../../shared/dcw-protocol'
|
|
14
|
+
import { AppError, ErrorCodes } from '../../../utils/errors'
|
|
15
|
+
|
|
16
|
+
/** 展开产物:执行节点创建所需的驱动字段(结构与 DCW 驱动 configFields 一致) */
|
|
17
|
+
export interface DerivedAccess {
|
|
18
|
+
driver: 'modbus-tcp' | 'modbus-rtu'
|
|
19
|
+
driverConfig: Record<string, string | number | boolean>
|
|
20
|
+
/** 节点工艺安全量程(取标定工程量程;float32 模式由调用方回退模板量程) */
|
|
21
|
+
engMin?: number
|
|
22
|
+
engMax?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 校验 + 展开:接入规格 → 驱动配置(非法配置在配置期即拒,不带病运行) */
|
|
26
|
+
export function deriveAccess(access: ParamAccessSpec): DerivedAccess {
|
|
27
|
+
const driver = access.driver ?? 'modbus-tcp'
|
|
28
|
+
const host = String(access.host ?? '').trim()
|
|
29
|
+
if (!host) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'access.host 必填:设备连接地址(PLC/网关 IP)')
|
|
30
|
+
const register = Number(access.register)
|
|
31
|
+
if (!Number.isInteger(register) || register < 40001 || register > 49999) {
|
|
32
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `access.register 须为 4xxxx 保持寄存器地址(当前: ${String(access.register)})`)
|
|
33
|
+
}
|
|
34
|
+
const conv = access.conversion
|
|
35
|
+
if (!conv || !['float32', 'int16-scaled', 'int32-scaled'].includes(conv?.mode)) {
|
|
36
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `access.conversion.mode 须为标准转换模式之一: float32 / int16-scaled / int32-scaled(当前: ${String(conv?.mode)})`)
|
|
37
|
+
}
|
|
38
|
+
const byteOrder = conv.byteOrder ?? 'big'
|
|
39
|
+
const base = {
|
|
40
|
+
host,
|
|
41
|
+
port: access.port == null ? 502 : Number(access.port),
|
|
42
|
+
unitId: access.unitId == null ? 1 : Number(access.unitId),
|
|
43
|
+
register,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (conv.mode === 'float32') {
|
|
47
|
+
// float32 直写:未配原始量程 → 驱动 raw=eng
|
|
48
|
+
return {
|
|
49
|
+
driver,
|
|
50
|
+
driverConfig: { ...base, dataType: 'float32', byteOrder },
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// int16/int32 线性标定:标定量程必填且 eng 跨度/raw 跨度非零
|
|
55
|
+
const engMin = Number(conv.engMin)
|
|
56
|
+
const engMax = Number(conv.engMax)
|
|
57
|
+
const rawMin = Number(conv.rawMin)
|
|
58
|
+
const rawMax = Number(conv.rawMax)
|
|
59
|
+
for (const [k, v] of [['engMin', engMin], ['engMax', engMax], ['rawMin', rawMin], ['rawMax', rawMax]] as const) {
|
|
60
|
+
if (!Number.isFinite(v)) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `转换模式 ${conv.mode} 需要完整线性标定:${k} 缺失或非数字`)
|
|
61
|
+
}
|
|
62
|
+
if (engMax === engMin) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `线性标定非法:engMax(${engMax}) 不得等于 engMin`)
|
|
63
|
+
if (rawMax === rawMin) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `线性标定非法:rawMax(${rawMax}) 不得等于 rawMin`)
|
|
64
|
+
return {
|
|
65
|
+
driver,
|
|
66
|
+
driverConfig: {
|
|
67
|
+
...base,
|
|
68
|
+
dataType: conv.mode === 'int16-scaled' ? 'int16' : 'int32',
|
|
69
|
+
byteOrder,
|
|
70
|
+
engMin,
|
|
71
|
+
engMax,
|
|
72
|
+
rawMin,
|
|
73
|
+
rawMax,
|
|
74
|
+
},
|
|
75
|
+
engMin,
|
|
76
|
+
engMax,
|
|
77
|
+
}
|
|
78
|
+
}
|