agentworkshop 0.7.39 → 0.7.41
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/{CY9LbPCI.js → BN16aRdQ.js} +1 -1
- package/.output/public/_nuxt/BWVhZntO.js +1 -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/{DUrecWPx.js → C0ZrS1KL.js} +2 -2
- package/.output/public/_nuxt/{C_OLzD09.js → C4oW1HsE.js} +1 -1
- package/.output/public/_nuxt/{CUDwzgLV.js → C4s_niy-.js} +9 -9
- package/.output/public/_nuxt/CFxtLwjp.js +1 -0
- package/.output/public/_nuxt/{Cec0Ua3F.js → CS50LB_l.js} +1 -1
- package/.output/public/_nuxt/CssgYAAL2.js +1 -0
- package/.output/public/_nuxt/D6mbFA-a.js +1 -0
- package/.output/public/_nuxt/D98NH8kG.js +1 -0
- package/.output/public/_nuxt/DGIuvyRN2.js +1 -0
- package/.output/public/_nuxt/DGbd-UiG.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/{45zuyAso.js → D_OTvQza.js} +1 -1
- package/.output/public/_nuxt/Dap5PmTL.js +2 -0
- package/.output/public/_nuxt/Dqw1lpr-.js +1 -0
- package/.output/public/_nuxt/DvLxILQP.js +1 -0
- package/.output/public/_nuxt/IREZ1eC2.js +1 -0
- package/.output/public/_nuxt/TownView.f-YOdXRs.css +1 -0
- package/.output/public/_nuxt/_id_.D9ymtHB5.css +1 -0
- package/.output/public/_nuxt/_id_.jf-_8E_c.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/78a295bb-2072-48a4-8b46-6d545fdae743.json +1 -0
- package/.output/public/_nuxt/ca4TJ3Op.js +1 -0
- package/.output/public/_nuxt/channel-templates.BOtahzPi.css +1 -0
- package/.output/public/_nuxt/daq.DjSgY94l.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/eqq3wWQF.js +18 -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 +2355 -1362
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{TownView-jVazYOjN.mjs → TownView--otsnr_7.mjs} +290 -208
- package/.output/server/chunks/build/TownView--otsnr_7.mjs.map +1 -0
- package/.output/server/chunks/build/{_id_-CfVosNg3.mjs → _id_-Brv-JSvr.mjs} +142 -141
- package/.output/server/chunks/build/_id_-Brv-JSvr.mjs.map +1 -0
- package/.output/server/chunks/build/{_id_-Hwr4rZpr.mjs → _id_-D9pl1OZy.mjs} +38 -36
- package/.output/server/chunks/build/_id_-D9pl1OZy.mjs.map +1 -0
- package/.output/server/chunks/build/{_wsId_-BVg3bB77.mjs → _wsId_-BASfDf0s.mjs} +295 -261
- package/.output/server/chunks/build/{_wsId_-BVg3bB77.mjs.map → _wsId_-BASfDf0s.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-DkMvD8sg.mjs} +193 -126
- package/.output/server/chunks/build/daq-DkMvD8sg.mjs.map +1 -0
- package/.output/server/chunks/build/{dcw-6vEYVeje.mjs → dcw-C1jgVJkO.mjs} +46 -46
- package/.output/server/chunks/build/{dcw-6vEYVeje.mjs.map → dcw-C1jgVJkO.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--dByNyhw.mjs} +35 -35
- package/.output/server/chunks/build/{logs-B-xhuqln.mjs.map → logs--dByNyhw.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-B0bUWnC6.mjs} +35 -19
- package/.output/server/chunks/build/pages-B0bUWnC6.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-CoQdblbZ.mjs} +38 -12
- package/.output/server/chunks/build/town-CoQdblbZ.mjs.map +1 -0
- package/.output/server/chunks/build/{useDaqStream-CIBaiGAl.mjs → useDaqStream-DfCFpb3p.mjs} +9 -3
- package/.output/server/chunks/build/useDaqStream-DfCFpb3p.mjs.map +1 -0
- package/.output/server/chunks/build/{useDcwStream-CBbiYnWe.mjs → useDcwStream-UPiX-3iO.mjs} +11 -2
- package/.output/server/chunks/build/useDcwStream-UPiX-3iO.mjs.map +1 -0
- 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/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_.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/index5.get.mjs +3 -1
- package/.output/server/chunks/routes/api/workshop/index5.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/index6.get.mjs +5 -1
- 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 +203 -39
- 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/useDaqStream.ts +7 -4
- package/app/composables/workshop/useDcwStream.ts +8 -2
- 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 +12 -7
- package/app/pages/daq/index.vue +216 -13
- package/app/pages/dcw/[id].vue +117 -32
- 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/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/plugins.en.md +33 -1
- package/docs/plugins.md +29 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/i18n/locales/en.ts +2 -0
- package/i18n/locales/zh-CN.ts +2 -0
- package/nuxt.config.ts +31 -0
- package/package.json +11 -2
- 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/_loop-biax-campaign.mjs +124 -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/test-serial-bridge.mjs +418 -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/e2e-interactive.mjs +328 -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/seed-industrial-demo.mjs +171 -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/index.get.ts +3 -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 +5 -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 +23 -10
- package/server/plugins/dev-stability-guard.ts +3 -1
- package/server/plugins/system-config.ts +6 -1
- package/server/plugins-builtin/serial-bridge/client.mjs +210 -0
- package/server/plugins-builtin/serial-bridge/drivers.mjs +409 -0
- package/server/plugins-builtin/serial-bridge/i18n.json +62 -0
- package/server/plugins-builtin/serial-bridge/index.mjs +247 -0
- package/server/plugins-builtin/serial-bridge/lib.mjs +208 -0
- package/server/plugins-builtin/serial-bridge/rtu.mjs +168 -0
- 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 +25 -5
- package/server/services/workshop/daq/daq-runtime.ts +5 -3
- package/server/services/workshop/daq/drivers.ts +141 -11
- 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 +147 -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 +90 -4
- 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/plugin-bridge.ts +45 -0
- package/server/services/workshop/dcw/recipe-rollback-manager.ts +13 -7
- package/server/services/workshop/plugins/host.mjs +30 -0
- 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/daq-protocol.ts +15 -0
- package/shared/dcw-protocol.ts +136 -0
- package/uno.config.ts +12 -1
- package/.output/public/_nuxt/8NMOXZOV.js +0 -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/DAe0h2MB.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_-CfVosNg3.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/useDaqStream-CIBaiGAl.mjs.map +0 -1
- package/.output/server/chunks/build/useDcwStream-CBbiYnWe.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
|
@@ -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,9 +11,10 @@
|
|
|
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, DcwParamInput, 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
|
+
import { attachDcwPluginBridge } from './plugin-bridge'
|
|
17
18
|
import { findDcwTemplate } from './dcw-templates'
|
|
18
19
|
import { getDeviceTwinRepo } from '../assets/device-twin.repo'
|
|
19
20
|
import { DcwNode } from './dcw-node'
|
|
@@ -22,6 +23,9 @@ import { DcwNodeRuntime } from './dcw-runtime'
|
|
|
22
23
|
import { getDcwRecipeRepo, type DcwWriteHistoryEntry } from './dcw-recipe.repo'
|
|
23
24
|
import { getDcwProductRepo } from './dcw-product.repo'
|
|
24
25
|
import { getDcwLineRepo } from './dcw-line.repo'
|
|
26
|
+
import { getDcwParamRepo } from './param-map.repo'
|
|
27
|
+
import { assertWithinLimits, limitsBreakdownOf } from './param-limits'
|
|
28
|
+
import { deriveAccess } from './param-conversion'
|
|
25
29
|
import { emitDcwWrite } from '@/server/services/workshop/plugins/host.mjs'
|
|
26
30
|
import { clearActiveLineRun, getActiveLineRun, getAllActiveLineRuns, setActiveLineRun } from './line-run'
|
|
27
31
|
import { getRecipeRollBackManager } from './recipe-rollback-manager'
|
|
@@ -31,6 +35,9 @@ import { daqRuntimeSettings } from '../settings'
|
|
|
31
35
|
/** 运维日志写下发防噪:同节点同值 10s 内的心跳重下发不重复入册 */
|
|
32
36
|
const opsWriteMemo = new Map<string, { eng: number, at: number }>()
|
|
33
37
|
|
|
38
|
+
// 写驱动插件桥(模块装载即挂;插件宿主先到则注册项排队、此处接管回放)
|
|
39
|
+
attachDcwPluginBridge()
|
|
40
|
+
|
|
34
41
|
/** 周期读网关默认间隔(节点 readIntervalMs=null 时生效;真实 PLC 建议按链路承载调整) */
|
|
35
42
|
const DEFAULT_READ_INTERVAL_MS = 5000
|
|
36
43
|
|
|
@@ -353,6 +360,9 @@ class DcwController {
|
|
|
353
360
|
semantics: input.semantics,
|
|
354
361
|
})
|
|
355
362
|
this.repo.insert(node)
|
|
363
|
+
// 工艺参数映射面:节点创建即自动生成同名映射(用户/Agent 的参数语义面;
|
|
364
|
+
// PLC 寻址细节仍封装在节点驱动配置内)。失败不阻断节点创建(可事后手工补建)。
|
|
365
|
+
getDcwParamRepo().ensureForNode(node)
|
|
356
366
|
this.syncRuntimes()
|
|
357
367
|
this.emitNodeChanged('added', node)
|
|
358
368
|
return node
|
|
@@ -431,6 +441,8 @@ class DcwController {
|
|
|
431
441
|
if (!node) throw new AppError(404, ErrorCodes.NOT_FOUND, `控制节点不存在: ${id}`)
|
|
432
442
|
this.repo.remove(id)
|
|
433
443
|
this.runtimes.delete(id)
|
|
444
|
+
// 级联清理:工艺参数映射面随执行节点删除(参数不存在悬空引用)
|
|
445
|
+
getDcwParamRepo().removeForNode(id)
|
|
434
446
|
// 级联清理:该节点的 Agent 绑定移除,挂起中的手动审批按失效收敛
|
|
435
447
|
void import('../agents/node-bindings.repo').then(({ getAgentNodeBindingRepo }) => {
|
|
436
448
|
const repo = getAgentNodeBindingRepo()
|
|
@@ -537,22 +549,21 @@ class DcwController {
|
|
|
537
549
|
}
|
|
538
550
|
// 调控闭环护栏(F1/F8):Agent 互斥(open 记录他人持有)+ 回退冷却方向性
|
|
539
551
|
getRecipeRollBackManager().beforeWrite(node, eng, meta)
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
552
|
+
// 写入限界分层联锁(param-limits):节点安全量程(结构层,驱动内 validateEng 结构性兜底)
|
|
553
|
+
// ∩ 工艺参数基准限界 ∩ 活动产品限界 ∩ 活动配方工艺窗口 —— 逐层收窄取交集,
|
|
554
|
+
// 手动 REST / Agent 工具 / 配方下发 / 回退四路共用本咽喉点,越界一律拒绝。
|
|
555
|
+
// D8 基准消融:仅 AW_BENCH_MODE=1 且 meta.benchArm='no-interlock'/'ungated' 时
|
|
556
|
+
// 旁路软联锁层(参数/产品/配方);结构层与硬量程校验结构性不可旁路;
|
|
557
|
+
// 生产模式(env 缺省)恒为 full 臂。
|
|
558
|
+
const benchNoInterlock = process.env.AW_BENCH_MODE === '1' && (meta?.benchArm === 'no-interlock' || meta?.benchArm === 'ungated')
|
|
559
|
+
// 配方下发路径(recipeRunId != null)跳过配方窗口层:下发值已在配方保存时对自身窗口校验,
|
|
560
|
+
// 且中途应用新配方不应被旧批次配方窗口误伤;产品/参数基准层对配方下发照常约束。
|
|
561
|
+
assertWithinLimits(node, eng, { includeSoft: !benchNoInterlock, skipRecipe: recipeRunId != null })
|
|
562
|
+
// D8: no-readback / ungated 臂 → 容差置 MAX,回读差异不致败(假成功语义,供 I2 消融)
|
|
563
|
+
const benchNoReadback = process.env.AW_BENCH_MODE === '1' && (meta?.benchArm === 'no-readback' || meta?.benchArm === 'ungated')
|
|
564
|
+
const benchToleranceOverride = benchNoReadback ? Number.MAX_VALUE : undefined
|
|
554
565
|
const prevValue = typeof node.value === 'number' ? node.value : null
|
|
555
|
-
const outcome = await rt.write(eng, recipeRunId)
|
|
566
|
+
const outcome = await rt.write(eng, recipeRunId, benchToleranceOverride)
|
|
556
567
|
// 调控闭环入册:仅成功写记锚/开记录(失败写不动账本 —— PLC 值未变更);
|
|
557
568
|
// 窗口聚合异步回填(F2)
|
|
558
569
|
if (outcome.ok !== false) {
|
|
@@ -618,6 +629,90 @@ class DcwController {
|
|
|
618
629
|
return this.testDriver(node.driver, node.driverConfig)
|
|
619
630
|
}
|
|
620
631
|
|
|
632
|
+
// ---------- 工艺参数映射面(用户/Agent 的唯一语义读写面)----------
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* 创建工艺参数映射。两条路径:
|
|
636
|
+
* - nodeId 绑定路径:映射指向既有执行节点;
|
|
637
|
+
* - access 接入路径:设备连接 + 寄存器 + **标准转换模式**一键建映射 —— 系统
|
|
638
|
+
* 按转换模式展开驱动配置并自动创建执行节点(float32 直写 / int16·int32 线性
|
|
639
|
+
* 标定),配置期一次性选定,运行期双向换算全自动。
|
|
640
|
+
*/
|
|
641
|
+
createParamMapping(input: DcwParamInput): DcwParamView {
|
|
642
|
+
const access = input.access
|
|
643
|
+
if (!access) {
|
|
644
|
+
const row = getDcwParamRepo().create(input)
|
|
645
|
+
return this.paramById(row.id)
|
|
646
|
+
}
|
|
647
|
+
if (input.nodeId) {
|
|
648
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'access 与 nodeId 互斥:access 路径由系统自动创建执行节点')
|
|
649
|
+
}
|
|
650
|
+
if (!input.templateRef) {
|
|
651
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'templateRef 必填:工艺参数需绑定语义模板(单位/量程/语义)')
|
|
652
|
+
}
|
|
653
|
+
const lineId = String(input.lineId ?? '')
|
|
654
|
+
if (lineId && !getDcwLineRepo().byId(lineId)) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lineId}`)
|
|
655
|
+
const derived = deriveAccess(access)
|
|
656
|
+
// 执行节点:驱动配置由转换模式展开(单一事实源);标定工程量程即节点安全量程
|
|
657
|
+
const node = this.create({
|
|
658
|
+
templateRef: input.templateRef,
|
|
659
|
+
name: input.name,
|
|
660
|
+
driver: derived.driver,
|
|
661
|
+
driverConfig: derived.driverConfig,
|
|
662
|
+
min: derived.engMin,
|
|
663
|
+
max: derived.engMax,
|
|
664
|
+
lineId,
|
|
665
|
+
})
|
|
666
|
+
// this.create() 已自动生成参数面;覆盖语义面(key/名称/基准限界)并留存转换模式
|
|
667
|
+
const auto = getDcwParamRepo().byNode(node.id)
|
|
668
|
+
const row = getDcwParamRepo().update(auto!.id, {
|
|
669
|
+
key: input.key,
|
|
670
|
+
name: input.name,
|
|
671
|
+
min: input.min ?? null,
|
|
672
|
+
max: input.max ?? null,
|
|
673
|
+
conversion: access.conversion,
|
|
674
|
+
})
|
|
675
|
+
return this.paramById(row.id)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
listParamViews(): DcwParamView[] {
|
|
679
|
+
return getDcwParamRepo().listViews()
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
paramById(id: string): DcwParamView {
|
|
683
|
+
const row = getDcwParamRepo().byId(id)
|
|
684
|
+
if (!row) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在: ${id}`)
|
|
685
|
+
const view = getDcwParamRepo().viewOf(row)
|
|
686
|
+
if (!view) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数 ${id} 的执行节点已不存在(映射悬空)`)
|
|
687
|
+
return view
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** 参数寻址写命令:工程量直写,限界联锁在 write() 咽喉点统一生效 */
|
|
691
|
+
async writeParam(id: string, value: number, meta?: DcwWriteMeta, recipeRunId: string | null = null) {
|
|
692
|
+
const row = getDcwParamRepo().byId(id)
|
|
693
|
+
const node = row ? this.repo.byId(row.nodeId) : undefined
|
|
694
|
+
if (!row || !node) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在(或执行节点已删除): ${id}`)
|
|
695
|
+
const outcome = await this.write(node.id, value, recipeRunId, meta)
|
|
696
|
+
return { ...outcome, param: getDcwParamRepo().viewOf(row) }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** 参数寻址读命令(读执行节点 PLC 值,标定解码为物理量) */
|
|
700
|
+
async readParam(id: string) {
|
|
701
|
+
const row = getDcwParamRepo().byId(id)
|
|
702
|
+
const node = row ? this.repo.byId(row.nodeId) : undefined
|
|
703
|
+
if (!row || !node) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在(或执行节点已删除): ${id}`)
|
|
704
|
+
const read = await this.readNow(node.id)
|
|
705
|
+
return { ...read, param: getDcwParamRepo().viewOf(row) }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** 参数当前有效写入限界剖面(分层 + 交集;展示/工具回包共用) */
|
|
709
|
+
paramLimitsOf(id: string) {
|
|
710
|
+
const row = getDcwParamRepo().byId(id)
|
|
711
|
+
const node = row ? this.repo.byId(row.nodeId) : undefined
|
|
712
|
+
if (!row || !node) throw new AppError(404, ErrorCodes.NOT_FOUND, `工艺参数不存在(或执行节点已删除): ${id}`)
|
|
713
|
+
return limitsBreakdownOf(node)
|
|
714
|
+
}
|
|
715
|
+
|
|
621
716
|
// ---------- Recipe 配方 ----------
|
|
622
717
|
|
|
623
718
|
listRecipes() {
|
|
@@ -950,6 +1045,7 @@ class DcwController {
|
|
|
950
1045
|
createProduct(input: ProductInput): ProductView {
|
|
951
1046
|
const lineId = String(input.lineId ?? '')
|
|
952
1047
|
if (lineId && !getDcwLineRepo().byId(lineId)) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lineId}`)
|
|
1048
|
+
assertProductParamLimits(lineId, input.paramLimits)
|
|
953
1049
|
return getDcwProductRepo().create({ ...input, lineId })
|
|
954
1050
|
}
|
|
955
1051
|
|
|
@@ -959,6 +1055,8 @@ class DcwController {
|
|
|
959
1055
|
if (!getDcwLineRepo().byId(lid)) throw new AppError(404, ErrorCodes.NOT_FOUND, `产线不存在: ${lid}`)
|
|
960
1056
|
}
|
|
961
1057
|
const prev = getDcwProductRepo().byId(id)
|
|
1058
|
+
const targetLine = String(patch.lineId ?? prev?.lineId ?? '')
|
|
1059
|
+
assertProductParamLimits(targetLine, patch.paramLimits)
|
|
962
1060
|
const updated = getDcwProductRepo().update(id, patch)
|
|
963
1061
|
// 产品换线 → 旗下配方产线归属级联(开跑校验按 recipe.lineId)
|
|
964
1062
|
if (prev && prev.lineId !== updated.lineId) {
|
|
@@ -1032,6 +1130,39 @@ class DcwController {
|
|
|
1032
1130
|
|
|
1033
1131
|
// ---------- 单例(HMR 存活) ----------
|
|
1034
1132
|
|
|
1133
|
+
/**
|
|
1134
|
+
* 产品级工艺参数限界校验:键必须命中该产品所属产线上的工艺参数映射
|
|
1135
|
+
* (防静默失效的拼错键);区间须 min<=max 且为有限数字。产品未挂产线时限界无
|
|
1136
|
+
* 寻址语境,拒绝非空限界。
|
|
1137
|
+
*/
|
|
1138
|
+
function assertProductParamLimits(lineId: string, paramLimits: ProductInput['paramLimits'] | undefined): void {
|
|
1139
|
+
if (paramLimits == null) return
|
|
1140
|
+
if (typeof paramLimits !== 'object' || Array.isArray(paramLimits)) {
|
|
1141
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'paramLimits 需为对象(键=工艺参数 key,值={min?,max?})')
|
|
1142
|
+
}
|
|
1143
|
+
const entries = Object.entries(paramLimits)
|
|
1144
|
+
if (entries.length === 0) return
|
|
1145
|
+
if (!lineId) {
|
|
1146
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, '产品未挂载产线,不可设定工艺参数限界(限界按产线内的工艺参数寻址)')
|
|
1147
|
+
}
|
|
1148
|
+
const lineKeys = new Set(
|
|
1149
|
+
getDcwParamRepo().listViews().filter(p => p.lineId === lineId).map(p => p.key),
|
|
1150
|
+
)
|
|
1151
|
+
for (const [key, range] of entries) {
|
|
1152
|
+
if (!lineKeys.has(key)) {
|
|
1153
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `产品限界的工艺参数键「${key}」在产线上不存在(可用的工艺参数:${[...lineKeys].join(', ') || '无 —— 先创建写控节点生成参数面'})`)
|
|
1154
|
+
}
|
|
1155
|
+
const min = range?.min == null ? null : Number(range.min)
|
|
1156
|
+
const max = range?.max == null ? null : Number(range.max)
|
|
1157
|
+
if ((range?.min != null && !Number.isFinite(min)) || (range?.max != null && !Number.isFinite(max))) {
|
|
1158
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `产品限界「${key}」的上下限需为数字`)
|
|
1159
|
+
}
|
|
1160
|
+
if (min != null && max != null && min > max) {
|
|
1161
|
+
throw new AppError(400, ErrorCodes.VALIDATION_ERROR, `产品限界「${key}」非法:min ${min} > max ${max}`)
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1035
1166
|
const g = globalThis as typeof globalThis & { __dcwController?: DcwController }
|
|
1036
1167
|
|
|
1037
1168
|
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
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createRequire } from 'node:module'
|
|
11
11
|
import type { DcwDriverKind } from '../../../../shared/dcw-protocol'
|
|
12
|
+
import { DCW_DRIVERS } from '../../../../shared/dcw-protocol'
|
|
12
13
|
import { AppError } from '../../../utils/errors'
|
|
13
14
|
import { classifyCommError, closeModbusSafely, decodeRegisters, evictModbusConn, evictOpcUaConn, getModbusConn, getOpcUaConn, modbusKey, registerOffset, withModbusConn } from '../daq/drivers'
|
|
14
15
|
|
|
@@ -524,7 +525,10 @@ export const mqttDcwDriver: DcwWriteDriver = {
|
|
|
524
525
|
// 失败/超时路径同样关闭连接(此前只在成功后 end,超时会泄漏 client +
|
|
525
526
|
// broker session),已关闭的 client 重复 end 幂等安全
|
|
526
527
|
await new Promise<void>((resolve) => {
|
|
527
|
-
|
|
528
|
+
// mqtt 的 DoneCallback 是 Node 风格 (error?: Error) => void;直接把 Promise 的 resolve
|
|
529
|
+
// 传进去会把 error 当成"完成值"兑现(错误被吞)。这里显式包一层,保持原语义:
|
|
530
|
+
// 回调一触发即完成,且与原来一样**不**因该 error 而 reject。
|
|
531
|
+
client.end(false, {}, () => resolve())
|
|
528
532
|
})
|
|
529
533
|
}
|
|
530
534
|
return {
|
|
@@ -552,7 +556,7 @@ export const mqttDcwDriver: DcwWriteDriver = {
|
|
|
552
556
|
})
|
|
553
557
|
c.once('connect', () => resolve(c))
|
|
554
558
|
c.once('error', err => reject(new Error(err.message)))
|
|
555
|
-
}).then(c => new Promise<void>(r => c.end(false, {}, r)))
|
|
559
|
+
}).then(c => new Promise<void>(r => c.end(false, {}, () => r())))
|
|
556
560
|
return { ok: true, message: `Broker 连接成功(${String(driverConfig.host)}:${Number(driverConfig.port ?? 1883)});未执行发布,避免误触发真实设备动作` }
|
|
557
561
|
}
|
|
558
562
|
catch (err) {
|
|
@@ -634,12 +638,94 @@ const REGISTRY: Record<DcwDriverKind, DcwWriteDriver> = {
|
|
|
634
638
|
'http': httpDcwDriver,
|
|
635
639
|
}
|
|
636
640
|
|
|
641
|
+
/**
|
|
642
|
+
* 插件写驱动注册表(经 ctx.dcw.registerWriteDriver;与数采侧 registerPluginDriver 对称)。
|
|
643
|
+
* globalThis 防 HMR 双实例;resolveDcwDriver 插件优先,热重载同名幂等覆盖。
|
|
644
|
+
* 驱动可携带可选 meta(label/configFields)进写控驱动目录(dcwDriverCatalog)。
|
|
645
|
+
*/
|
|
646
|
+
export interface PluginWriteDriverMeta {
|
|
647
|
+
kind: string
|
|
648
|
+
label: string
|
|
649
|
+
status: 'builtin' | 'real' | 'planned'
|
|
650
|
+
configFields?: import('../../../../shared/daq-protocol').DriverConfigField[]
|
|
651
|
+
plugin: true
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const g_plugins = globalThis as typeof globalThis & { __dcwPluginDrivers?: Map<string, DcwWriteDriver> }
|
|
655
|
+
function pluginRegistry(): Map<string, DcwWriteDriver> {
|
|
656
|
+
return g_plugins.__dcwPluginDrivers ??= new Map()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const g_metas = globalThis as typeof globalThis & { __dcwPluginDriverMetas?: Map<string, PluginWriteDriverMeta> }
|
|
660
|
+
function pluginMetaRegistry(): Map<string, PluginWriteDriverMeta> {
|
|
661
|
+
return g_metas.__dcwPluginDriverMetas ??= new Map()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function registerPluginWriteDriver(driver: DcwWriteDriver): void {
|
|
665
|
+
if (driver.kind in REGISTRY) console.warn(`[dcw-drivers] 插件写驱动覆盖内置:「${driver.kind}」`)
|
|
666
|
+
pluginRegistry().set(driver.kind, driver)
|
|
667
|
+
const meta = (driver as DcwWriteDriver & { meta?: Omit<PluginWriteDriverMeta, 'kind' | 'plugin'> }).meta
|
|
668
|
+
if (meta && typeof meta.label === 'string') {
|
|
669
|
+
pluginMetaRegistry().set(driver.kind, {
|
|
670
|
+
kind: driver.kind,
|
|
671
|
+
label: meta.label,
|
|
672
|
+
status: meta.status === 'builtin' || meta.status === 'planned' ? meta.status : 'real',
|
|
673
|
+
configFields: Array.isArray(meta.configFields) ? meta.configFields : [],
|
|
674
|
+
plugin: true,
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
pluginMetaRegistry().delete(driver.kind)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function listPluginWriteDrivers(): string[] {
|
|
683
|
+
return [...pluginRegistry().keys()]
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** 插件写驱动自描述目录(写控驱动目录合并端点用) */
|
|
687
|
+
export function listPluginWriteDriverMetas(): PluginWriteDriverMeta[] {
|
|
688
|
+
return [...pluginMetaRegistry().values()]
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** 清空插件写驱动与其自描述目录(插件宿主热重载前调用;与数采侧 clearPluginDrivers 对称) */
|
|
692
|
+
export function clearPluginWriteDrivers(): void {
|
|
693
|
+
pluginRegistry().clear()
|
|
694
|
+
pluginMetaRegistry().clear()
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 写控驱动目录(内置 DCW_DRIVERS + 插件自描述合并;同名插件条目覆盖内置)。
|
|
699
|
+
* dcw index.get 以 `drivers` 下发,前端写控节点向导据此渲染。
|
|
700
|
+
*/
|
|
701
|
+
export async function dcwDriverCatalog(): Promise<Array<{
|
|
702
|
+
kind: string
|
|
703
|
+
label: string
|
|
704
|
+
status: 'builtin' | 'real' | 'planned'
|
|
705
|
+
configFields: import('../../../../shared/daq-protocol').DriverConfigField[]
|
|
706
|
+
plugin?: boolean
|
|
707
|
+
}>> {
|
|
708
|
+
const out: Array<{ kind: string, label: string, status: 'builtin' | 'real' | 'planned', configFields: import('../../../../shared/daq-protocol').DriverConfigField[], plugin?: boolean }> = DCW_DRIVERS.map(d => ({
|
|
709
|
+
kind: d.kind,
|
|
710
|
+
label: d.label,
|
|
711
|
+
status: d.status,
|
|
712
|
+
configFields: d.configFields,
|
|
713
|
+
}))
|
|
714
|
+
for (const m of listPluginWriteDriverMetas()) {
|
|
715
|
+
const i = out.findIndex(d => d.kind === m.kind)
|
|
716
|
+
const entry = { kind: m.kind, label: m.label, status: m.status, configFields: m.configFields ?? [], plugin: true }
|
|
717
|
+
if (i >= 0) out[i] = entry
|
|
718
|
+
else out.push(entry)
|
|
719
|
+
}
|
|
720
|
+
return out
|
|
721
|
+
}
|
|
722
|
+
|
|
637
723
|
export function normalizeDcwDriverKind(kind: string): DcwDriverKind {
|
|
638
724
|
if (kind === 'modbus') return 'modbus-tcp'
|
|
639
725
|
if (kind === 'rtu' || kind === 'modbus-rtu-tcp') return 'modbus-rtu'
|
|
640
|
-
return (kind in REGISTRY ? kind : 'mock') as DcwDriverKind
|
|
726
|
+
return ((kind in REGISTRY || pluginRegistry().has(kind) ? kind : 'mock')) as DcwDriverKind
|
|
641
727
|
}
|
|
642
728
|
|
|
643
729
|
export function resolveDcwDriver(kind: DcwDriverKind): DcwWriteDriver {
|
|
644
|
-
return REGISTRY[kind] ?? mockDcwDriver
|
|
730
|
+
return pluginRegistry().get(kind) ?? REGISTRY[kind] ?? mockDcwDriver
|
|
645
731
|
}
|
|
@@ -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
|
+
}
|