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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose-built ENGLISH-locale fixture for the IEEE TII Figure 2 walkthrough.
|
|
3
|
+
*
|
|
4
|
+
* The shared production instance is full of Chinese-named test data; we must NOT
|
|
5
|
+
* rename or delete anything. Instead this script creates a small additive fixture
|
|
6
|
+
* with English entity names via the platform's own REST API, so the four
|
|
7
|
+
* walkthrough screenshots contain English strings only:
|
|
8
|
+
*
|
|
9
|
+
* - line "Cast-film line A"
|
|
10
|
+
* - DAQ nodes mock + a real Modbus-TCP node on the PLC simulator, one line
|
|
11
|
+
* - device "Die-head heater" twin, bound by a DCW node with a 150-200 C window
|
|
12
|
+
* - agents English lead + workers on the `omp` harness
|
|
13
|
+
* - team "Cast-film control team" (lead + 2 workers)
|
|
14
|
+
*
|
|
15
|
+
* Idempotent: re-running reuses rows with the same names. Prints a JSON summary.
|
|
16
|
+
*
|
|
17
|
+
* Usage: NO_PROXY='127.0.0.1,localhost' node scripts/seed-walkthrough-fixture.mjs
|
|
18
|
+
*/
|
|
19
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3005'
|
|
20
|
+
|
|
21
|
+
const login = async (e, p) =>
|
|
22
|
+
(await (await fetch(`${BASE}/api/users/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: e, password: p }) })).json()).data.token
|
|
23
|
+
|
|
24
|
+
const T = await login('admin@awshop.local', 'admin123')
|
|
25
|
+
|
|
26
|
+
const api = async (method, path, body) => {
|
|
27
|
+
const r = await fetch(`${BASE}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { Authorization: `Bearer ${T}`, ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
30
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
+
})
|
|
32
|
+
const j = await r.json().catch(() => ({}))
|
|
33
|
+
if (!r.ok || j.code !== 0) throw new Error(`${method} ${path} -> ${r.status} ${JSON.stringify(j).slice(0, 200)}`)
|
|
34
|
+
return j.data
|
|
35
|
+
}
|
|
36
|
+
const get = p => api('GET', p)
|
|
37
|
+
const post = (p, b) => api('POST', p, b)
|
|
38
|
+
|
|
39
|
+
// ── 1. production line ────────────────────────────────────────────────────
|
|
40
|
+
const LINE_NAME = 'Cast-film line A'
|
|
41
|
+
const lines = (await get('/api/workshop/dcw/lines')).lines
|
|
42
|
+
let line = lines.find(l => l.name === LINE_NAME)
|
|
43
|
+
if (!line) line = (await post('/api/workshop/dcw/lines', { name: LINE_NAME, color: '#f4c542' })).line
|
|
44
|
+
console.log(`line : ${line.id} ${line.name}`)
|
|
45
|
+
|
|
46
|
+
// ── 2. DAQ nodes on that line (mock + real Modbus-TCP share one loop) ─────
|
|
47
|
+
const daq = await get('/api/workshop/daq')
|
|
48
|
+
const haveNode = n => daq.nodes.find(x => x.name === n)
|
|
49
|
+
const mkNode = async (spec) => {
|
|
50
|
+
const existing = haveNode(spec.name)
|
|
51
|
+
if (existing) return existing
|
|
52
|
+
return (await post('/api/workshop/daq', { enabled: true, intervalMs: 1000, lineId: line.id, ...spec })).node
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nTemp = await mkNode({
|
|
56
|
+
name: 'Melt temperature', templateRef: 'daq-temp-tc', driver: 'mock',
|
|
57
|
+
unit: '\u00B0C', decimals: 1, min: 150, max: 200, warnLow: 155, warnHigh: 195,
|
|
58
|
+
})
|
|
59
|
+
const nSpeed = await mkNode({
|
|
60
|
+
name: 'Line speed', templateRef: 'daq-line-encoder', driver: 'mock',
|
|
61
|
+
unit: 'm/min', decimals: 0, min: 280, max: 360, warnLow: 290, warnHigh: 350,
|
|
62
|
+
})
|
|
63
|
+
const nPress = await mkNode({
|
|
64
|
+
name: 'Melt pressure', templateRef: 'daq-pressure-tx', driver: 'mock',
|
|
65
|
+
unit: 'MPa', decimals: 2, min: 0.6, max: 1.2, warnLow: 0.65, warnHigh: 1.15,
|
|
66
|
+
})
|
|
67
|
+
// real Modbus-TCP against the PLC simulator's "extruder" slave (melt-temp @ 40001)
|
|
68
|
+
const nModbus = await mkNode({
|
|
69
|
+
name: 'Die-head temperature', templateRef: 'daq-temp-tc', driver: 'modbus-tcp',
|
|
70
|
+
unit: '\u00B0C', decimals: 2, min: 0, max: 400, warnLow: 150, warnHigh: 260,
|
|
71
|
+
driverConfig: { host: '127.0.0.1', port: 16040, unitId: 1, register: 40001, dataType: 'float32', byteOrder: 'big' },
|
|
72
|
+
})
|
|
73
|
+
console.log(`daq : temp=${nTemp.id} speed=${nSpeed.id} pressure=${nPress.id} modbus=${nModbus.id}`)
|
|
74
|
+
|
|
75
|
+
// ── 3. device twin + DCW control node with a recipe window ────────────────
|
|
76
|
+
const twins = (await get('/api/workshop/device-twins')).twins
|
|
77
|
+
let twin = twins.find(t => t.name === 'Die-head heater')
|
|
78
|
+
if (!twin) twin = (await post('/api/workshop/device-twins', { name: 'Die-head heater', kind: 'device', controls: ['setpoint'] })).twin
|
|
79
|
+
console.log(`twin : ${twin.id} ${twin.name}`)
|
|
80
|
+
|
|
81
|
+
const dcw = await get('/api/workshop/dcw')
|
|
82
|
+
let dcwNode = dcw.nodes.find(n => n.name === 'Die-head setpoint')
|
|
83
|
+
if (!dcwNode) {
|
|
84
|
+
dcwNode = (await post('/api/workshop/dcw', {
|
|
85
|
+
name: 'Die-head setpoint', templateRef: 'dcw-temp-sp', driver: 'mock',
|
|
86
|
+
unit: '\u00B0C', decimals: 1, min: 150, max: 200,
|
|
87
|
+
holdIntervalMs: 5000, readIntervalMs: 5000, enabled: true,
|
|
88
|
+
lineId: line.id, deviceIds: [twin.id],
|
|
89
|
+
})).node
|
|
90
|
+
}
|
|
91
|
+
console.log(`dcw : ${dcwNode.id} ${dcwNode.name} bound=${twin.id} window=${dcwNode.min}-${dcwNode.max}`)
|
|
92
|
+
|
|
93
|
+
// ── 4. English agents + team (one lead, harness-backed workers) ───────────
|
|
94
|
+
const teams0 = await get('/api/workshop/teams')
|
|
95
|
+
let team = teams0.find(t => t.name === 'Cast-film control team')
|
|
96
|
+
if (!team) team = await post('/api/workshop/teams', { name: 'Cast-film control team', description: 'Cast-film process lead plus DAQ and modelling workers.', visibility: 'public' })
|
|
97
|
+
|
|
98
|
+
const teamId = team.id
|
|
99
|
+
const existingTeam = (await get('/api/workshop/teams')).find(t => t.id === teamId)
|
|
100
|
+
const memberNames = new Set((existingTeam?.members ?? []).map(m => m.name))
|
|
101
|
+
|
|
102
|
+
const AGENTS = [
|
|
103
|
+
{ name: 'Cast-film process lead', harness: 'omp', role: 'lead' },
|
|
104
|
+
{ name: 'DAQ engineer', harness: 'omp', role: 'worker' },
|
|
105
|
+
{ name: 'Model trainer', harness: 'omp', role: 'worker' },
|
|
106
|
+
]
|
|
107
|
+
for (const a of AGENTS) {
|
|
108
|
+
if (memberNames.has(a.name)) continue
|
|
109
|
+
const agent = await post('/api/workshop/agents', { name: a.name, harness: a.harness, visibility: 'public' })
|
|
110
|
+
await post(`/api/workshop/teams/${teamId}/members`, { agentId: agent.id, role: a.role })
|
|
111
|
+
}
|
|
112
|
+
const finalTeam = (await get('/api/workshop/teams')).find(t => t.id === teamId)
|
|
113
|
+
console.log(`team : ${teamId} ${finalTeam.name} members=${(finalTeam.members ?? []).map(m => `${m.role}:${m.name}:${m.harness}`).join(', ')}`)
|
|
114
|
+
|
|
115
|
+
// ── 5. start the line so DAQ acquisition is genuinely live ────────────────
|
|
116
|
+
// DAQ sampling is gated on an active per-line batch (recipe run). Without a run
|
|
117
|
+
// the nodes report `offline` and the panel-1 claim "share one acquisition loop"
|
|
118
|
+
// would be untrue. Create a product + recipe carrying one setpoint param and
|
|
119
|
+
// start the line; idempotent via the line's active-run flag.
|
|
120
|
+
const lineStates = (await get('/api/workshop/dcw/lines')).states
|
|
121
|
+
const alreadyRunning = lineStates.find(s => s.lineId === line.id && s.active)
|
|
122
|
+
if (alreadyRunning) {
|
|
123
|
+
console.log(`run : already active ${alreadyRunning.runId}`)
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const dcwState = await get('/api/workshop/dcw')
|
|
127
|
+
let product = (dcwState.products ?? []).find(p => p.name === 'Cast-film roll')
|
|
128
|
+
if (!product) product = (await post('/api/workshop/dcw/products', { name: 'Cast-film roll', lineId: line.id })).product
|
|
129
|
+
let recipe = (dcwState.recipes ?? []).find(r => r.name === 'Cast-film recipe' && r.lineId === line.id)
|
|
130
|
+
if (!recipe) {
|
|
131
|
+
recipe = (await post('/api/workshop/dcw/recipes', {
|
|
132
|
+
productId: product.id,
|
|
133
|
+
name: 'Cast-film recipe',
|
|
134
|
+
description: 'Cast-film line A baseline recipe.',
|
|
135
|
+
params: [{ nodeId: dcwNode.id, value: 178, min: 150, max: 200 }],
|
|
136
|
+
daqWindows: [{ nodeId: nTemp.id, min: 150, max: 200 }],
|
|
137
|
+
})).recipe
|
|
138
|
+
}
|
|
139
|
+
await post(`/api/workshop/dcw/lines/${line.id}/start`, { recipeId: recipe.id })
|
|
140
|
+
console.log(`run : started with recipe ${recipe.id} (${recipe.name})`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('\nFIXTURE ' + JSON.stringify({ lineId: line.id, lineName: line.name, modbusNodeId: nModbus.id, dcwNodeId: dcwNode.id, twinId: twin.id, teamId }))
|
|
@@ -60,6 +60,14 @@ console.log('\n━━━ 3. 稳定性护栏的严重性判定(哪类异常不该
|
|
|
60
60
|
check('undici UND_ERR_CONNECT_TIMEOUT → upstream', classifyFatalCandidate(err('UND_ERR_CONNECT_TIMEOUT')) === 'upstream')
|
|
61
61
|
check('无 code 但 message 带 ECONNREFUSED → upstream',
|
|
62
62
|
classifyFatalCandidate(new Error('connect ECONNREFUSED 127.0.0.1:1502')) === 'upstream')
|
|
63
|
+
// 实测事故 2:工业驱动库(Modbus/OPC UA 客户端)超时时只给可读 message、不带 errno code,
|
|
64
|
+
// 旧实现据此判 fatal → 生产实例运行中 exit 1(平台带着数采/数控一起挂掉)。
|
|
65
|
+
check('无 code,message "TCP Connection Timed Out" → upstream',
|
|
66
|
+
classifyFatalCandidate(new Error('TCP Connection Timed Out')) === 'upstream')
|
|
67
|
+
check('无 code,message "connection timed out" → upstream',
|
|
68
|
+
classifyFatalCandidate(new Error('connect error: connection timed out')) === 'upstream')
|
|
69
|
+
check('无 code,message "socket hang up" → upstream',
|
|
70
|
+
classifyFatalCandidate(new Error('socket hang up')) === 'upstream')
|
|
63
71
|
|
|
64
72
|
// 其余既有分级不得回退
|
|
65
73
|
check('ECONNRESET(客户端硬断)→ socket', classifyFatalCandidate(err('ECONNRESET')) === 'socket')
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回归测试 —— serial-bridge 串口协议插件(纯协议层 + RTU 事务客户端 + 假端口驱动契约 + 插件入口契约)。
|
|
3
|
+
* 运行: node scripts/test-serial-bridge.mjs
|
|
4
|
+
* 退出码: 0 全通过 / 1 有失败
|
|
5
|
+
*
|
|
6
|
+
* 无需真实串口硬件:RTU 客户端(rtu.mjs)以假 SerialPort(内存从站应答)驱动,
|
|
7
|
+
* 驱动工厂(drivers.mjs)以注入 opener 构建,ASCII 行用假行端口;插件入口用桩 ctx
|
|
8
|
+
* 验证注册契约与 API 路由。
|
|
9
|
+
*/
|
|
10
|
+
import { Buffer } from 'node:buffer'
|
|
11
|
+
import {
|
|
12
|
+
crc16Modbus,
|
|
13
|
+
buildRtuFrame,
|
|
14
|
+
parseRtuFrame,
|
|
15
|
+
decodeRegisters,
|
|
16
|
+
encodeWords,
|
|
17
|
+
engToRaw,
|
|
18
|
+
rawToEng,
|
|
19
|
+
extractLineValue,
|
|
20
|
+
normalizeSerialConfig,
|
|
21
|
+
registerOffset,
|
|
22
|
+
} from '../server/plugins-builtin/serial-bridge/lib.mjs'
|
|
23
|
+
import { createSerialDrivers } from '../server/plugins-builtin/serial-bridge/drivers.mjs'
|
|
24
|
+
import { createRtuClient } from '../server/plugins-builtin/serial-bridge/rtu.mjs'
|
|
25
|
+
import serialBridge from '../server/plugins-builtin/serial-bridge/index.mjs'
|
|
26
|
+
|
|
27
|
+
let pass = 0
|
|
28
|
+
let fail = 0
|
|
29
|
+
const check = (name, ok, detail = '') => {
|
|
30
|
+
if (ok) {
|
|
31
|
+
pass++
|
|
32
|
+
console.log(` PASS ${name}`)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
fail++
|
|
36
|
+
console.log(` FAIL ${name}${detail ? ' — ' + detail : ''}`)
|
|
37
|
+
}
|
|
38
|
+
const approx = (a, b, eps = 1e-4) => Math.abs(a - b) <= eps
|
|
39
|
+
|
|
40
|
+
console.log('\n━━━ 1. 纯协议层(lib.mjs) ━━━')
|
|
41
|
+
{
|
|
42
|
+
// Modbus CRC16 已知向量:01 03 00 00 00 02 → C4 0B(Modbus 官方工具可复算)
|
|
43
|
+
const crc = crc16Modbus(Buffer.from([0x01, 0x03, 0x00, 0x00, 0x00, 0x02]))
|
|
44
|
+
check('CRC16 已知向量 01 03 00 00 00 02 → 0xC40B', crc === 0x0BC4, `got 0x${crc.toString(16)}`)
|
|
45
|
+
|
|
46
|
+
const frame = buildRtuFrame(1, 3, [0x00, 0x00, 0x00, 0x02])
|
|
47
|
+
check('RTU 帧构造(CRC lo/hi 序)', frame.length === 8 && frame[6] === 0xC4 && frame[7] === 0x0B, frame.toString('hex'))
|
|
48
|
+
|
|
49
|
+
const parsed = parseRtuFrame(frame)
|
|
50
|
+
check('RTU 帧解析(addr/func/data)', parsed.addr === 1 && parsed.func === 3 && parsed.data.length === 4)
|
|
51
|
+
const bad = Buffer.from(frame)
|
|
52
|
+
bad[2] ^= 0xFF
|
|
53
|
+
check('RTU 帧篡改 → crc 错误', parseRtuFrame(bad).error === 'crc')
|
|
54
|
+
check('RTU 短帧 → short', parseRtuFrame(Buffer.from([1, 2])).error === 'short')
|
|
55
|
+
|
|
56
|
+
// 寄存器编解码:float32 123.456 全字节序 roundtrip(int16 零点用带符号验证)
|
|
57
|
+
for (const byteOrder of ['big', 'little', 'wordSwap']) {
|
|
58
|
+
const words = encodeWords(123.456, 'float32', byteOrder)
|
|
59
|
+
const back = decodeRegisters(words, 'float32', byteOrder)
|
|
60
|
+
check(`float32 ${byteOrder} 编解码互逆`, approx(words.length === 2 ? back : back, 123.456), `got ${back}`)
|
|
61
|
+
}
|
|
62
|
+
const i16 = decodeRegisters([0xFFFE], 'int16', 'big')
|
|
63
|
+
check('int16 带符号解码 0xFFFE → -2', i16 === -2, `got ${i16}`)
|
|
64
|
+
const u32 = decodeRegisters(encodeWords(3000000123, 'uint32', 'big'), 'uint32', 'big')
|
|
65
|
+
check('uint32 3000000123 roundtrip', u32 === 3000000123, `got ${u32}`)
|
|
66
|
+
|
|
67
|
+
// 与内置 modbus 驱动同规约:40001 → 偏移 0,30001 → 0
|
|
68
|
+
check('registerOffset 40001→0 / 30001→0', registerOffset(40001, 'holding') === 0 && registerOffset(30001, 'input') === 0)
|
|
69
|
+
|
|
70
|
+
// 工程量↔原始值线性映射(0~27648 ↔ 0~100)
|
|
71
|
+
const cfg = { rawMin: 0, rawMax: 27648, engMin: 0, engMax: 100 }
|
|
72
|
+
const raw = engToRaw(50, cfg)
|
|
73
|
+
check('engToRaw 50 → 13824', approx(raw, 13824, 0.01), `got ${raw}`)
|
|
74
|
+
check('rawToEng 互逆', approx(rawToEng(raw, cfg), 50, 1e-6))
|
|
75
|
+
check('未配置量程直传', engToRaw(7.5, {}) === 7.5)
|
|
76
|
+
|
|
77
|
+
// ASCII 行取值
|
|
78
|
+
check('裸数字行', extractLineValue('25.3\n', '') === 25.3)
|
|
79
|
+
check('JSON 无路径 → null(需配置 jsonPath,取值路径语义显式)', extractLineValue('{"temp": 21.5}', '') === null)
|
|
80
|
+
check('JSON jsonPath', extractLineValue('{"data":{"temp":21.5}}', 'data.temp') === 21.5)
|
|
81
|
+
check('JSON 数组下标路径', extractLineValue('{"arr":[1,2,9.5]}', 'arr.2') === 9.5)
|
|
82
|
+
check('垃圾行 → null(跳帧)', extractLineValue('ERROR OVERHEAT', '') === null)
|
|
83
|
+
check('空行 → null', extractLineValue(' \n', '') === null)
|
|
84
|
+
|
|
85
|
+
// 配置规范化
|
|
86
|
+
const n1 = normalizeSerialConfig({ path: 'COM3', register: 40001 })
|
|
87
|
+
check('缺省补齐(mode/baud/type/byteOrder)', n1.errors.length === 0 && n1.cfg.mode === 'modbus-rtu' && n1.cfg.baudRate === 9600 && n1.cfg.dataType === 'float32' && n1.cfg.byteOrder === 'big')
|
|
88
|
+
const n2 = normalizeSerialConfig({ mode: 'ascii-line' })
|
|
89
|
+
check('ascii-line 不要求 register,仍要求 path', n2.errors.length === 1 && /path/.test(n2.errors[0]))
|
|
90
|
+
const n3 = normalizeSerialConfig({ path: 'COM3', register: 1, mode: 'nope' })
|
|
91
|
+
check('非法 mode 报错且回退 modbus-rtu', n3.cfg.mode === 'modbus-rtu' && n3.errors.some(e => /mode/.test(e)))
|
|
92
|
+
const n4 = normalizeSerialConfig({ path: 'COM3', register: 40001, scale: 0 })
|
|
93
|
+
check('scale=0 拒绝(除零保护)', n4.cfg.scale === 1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('\n━━━ 2. RTU 事务客户端(rtu.mjs,假 SerialPort 从站) ━━━')
|
|
97
|
+
{
|
|
98
|
+
/** 内存 RTU 从站:以 EventEmitter 风格的假 SerialPort,写请求即回应答帧 */
|
|
99
|
+
function makeFakeSerialSlave({ unitId = 7, holding = new Map(), corrupt = false, silent = false, badAddr = false } = {}) {
|
|
100
|
+
const listeners = { data: [], error: [], close: [], open: [] }
|
|
101
|
+
const port = {
|
|
102
|
+
isOpen: false,
|
|
103
|
+
on(ev, fn) { listeners[ev]?.push(fn) },
|
|
104
|
+
once(ev, fn) { listeners[ev]?.push(fn) },
|
|
105
|
+
write(buf, cb) {
|
|
106
|
+
setImmediate(() => cb?.())
|
|
107
|
+
if (silent) return
|
|
108
|
+
const req = Buffer.from(buf)
|
|
109
|
+
const func = req[1]
|
|
110
|
+
const respAddr = badAddr ? unitId + 1 : unitId
|
|
111
|
+
let resp
|
|
112
|
+
if (func === 3 || func === 4) {
|
|
113
|
+
const offset = req.readUInt16BE(2)
|
|
114
|
+
const count = req.readUInt16BE(4)
|
|
115
|
+
const data = Buffer.alloc(count * 2)
|
|
116
|
+
for (let i = 0; i < count; i++) data.writeUInt16BE(holding.get(offset + i) ?? 0, i * 2)
|
|
117
|
+
resp = Buffer.concat([Buffer.from([respAddr, func, count * 2]), data])
|
|
118
|
+
}
|
|
119
|
+
else if (func === 6) {
|
|
120
|
+
resp = Buffer.from(req.subarray(0, 6))
|
|
121
|
+
}
|
|
122
|
+
else if (func === 16) {
|
|
123
|
+
resp = Buffer.from([respAddr, func, req[2], req[3], req[4], req[5]])
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
resp = Buffer.from([respAddr, func | 0x80, 1])
|
|
127
|
+
}
|
|
128
|
+
if (corrupt) resp[resp.length - 1] ^= 0xFF
|
|
129
|
+
const c = Buffer.alloc(2)
|
|
130
|
+
c.writeUInt16LE(crc16Modbus(resp))
|
|
131
|
+
const frame = Buffer.concat([resp, c])
|
|
132
|
+
if (corrupt) frame[frame.length - 1] ^= 0xFF
|
|
133
|
+
setImmediate(() => listeners.data.forEach(fn => fn(frame)))
|
|
134
|
+
},
|
|
135
|
+
close(cb) {
|
|
136
|
+
port.isOpen = false
|
|
137
|
+
listeners.close.forEach(fn => fn())
|
|
138
|
+
setImmediate(() => cb?.())
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
// autoOpen 语义:构造即异步发 open 事件(与真实 serialport 一致)
|
|
142
|
+
setImmediate(() => {
|
|
143
|
+
port.isOpen = true
|
|
144
|
+
listeners.open.forEach(fn => fn())
|
|
145
|
+
})
|
|
146
|
+
return port
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const regs = new Map([[0, 0x41D4], [1, 0x0000]]) // float32 big 26.5
|
|
150
|
+
/** rtu.mjs 需要的是「类」(open() 内 new SerialPort(opts));构造器返回实例对象 */
|
|
151
|
+
const mkClass = (opts = {}) => function FakeSerialPortClass() {
|
|
152
|
+
return makeFakeSerialSlave({ holding: regs, ...opts })
|
|
153
|
+
}
|
|
154
|
+
const mk = (opts = {}) => createRtuClient(mkClass(opts), { path: 'FAKE', baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none', unitId: 7, timeoutMs: 400 })
|
|
155
|
+
|
|
156
|
+
const client = mk()
|
|
157
|
+
await client.open()
|
|
158
|
+
check('RTU open 后 isOpen', client.isOpen === true)
|
|
159
|
+
|
|
160
|
+
const r1 = await client.readHoldingRegisters(0, 2)
|
|
161
|
+
check('RTU FC03 读保持寄存器(float32 字序)', r1.data.length === 2 && r1.data[0] === 0x41D4 && r1.data[1] === 0x0000, JSON.stringify(r1))
|
|
162
|
+
|
|
163
|
+
const w1 = await client.writeRegister(5, 0xABCD)
|
|
164
|
+
check('RTU FC06 写单寄存器(回显)', w1.value === 0xABCD, JSON.stringify(w1))
|
|
165
|
+
|
|
166
|
+
const w2 = await client.writeRegisters(10, [1, 2, 3])
|
|
167
|
+
check('RTU FC16 写多寄存器', w2.length === 3, JSON.stringify(w2))
|
|
168
|
+
await client.close()
|
|
169
|
+
check('RTU close 后 isOpen=false', client.isOpen === false)
|
|
170
|
+
|
|
171
|
+
// 异常帧:专用假从站对一切请求回 func|0x80 + code=2(带合法 CRC)
|
|
172
|
+
function ExceptionPortClass() {
|
|
173
|
+
const __l = { data: [], error: [], close: [], open: [] }
|
|
174
|
+
const port = {
|
|
175
|
+
isOpen: false,
|
|
176
|
+
on(ev, fn) { __l[ev].push(fn) },
|
|
177
|
+
once(ev, fn) { __l[ev].push(fn) },
|
|
178
|
+
write(buf, cb) {
|
|
179
|
+
setImmediate(() => cb?.())
|
|
180
|
+
const body = Buffer.from([7, Buffer.from(buf)[1] | 0x80, 2])
|
|
181
|
+
const c = Buffer.alloc(2)
|
|
182
|
+
c.writeUInt16LE(crc16Modbus(body))
|
|
183
|
+
setImmediate(() => __l.data.forEach(fn => fn(Buffer.concat([body, c]))))
|
|
184
|
+
},
|
|
185
|
+
close(cb) { setImmediate(() => cb?.()) },
|
|
186
|
+
}
|
|
187
|
+
setImmediate(() => {
|
|
188
|
+
port.isOpen = true
|
|
189
|
+
__l.open.forEach(fn => fn())
|
|
190
|
+
})
|
|
191
|
+
return port
|
|
192
|
+
}
|
|
193
|
+
const exReal = createRtuClient(ExceptionPortClass, { path: 'FAKE', baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none', unitId: 7, timeoutMs: 400 })
|
|
194
|
+
await exReal.open()
|
|
195
|
+
let exErr = null
|
|
196
|
+
await exReal.readHoldingRegisters(0, 1).catch(e => (exErr = e.message))
|
|
197
|
+
check('RTU 异常帧 → exception code 错误', exErr?.includes('exception code=2'), exErr)
|
|
198
|
+
|
|
199
|
+
const crcClient = mk({ corrupt: true })
|
|
200
|
+
await crcClient.open()
|
|
201
|
+
let crcErr = null
|
|
202
|
+
await crcClient.readHoldingRegisters(0, 1).catch(e => (crcErr = e.message))
|
|
203
|
+
check('RTU CRC 篡改 → CRC 错误', crcErr?.includes('CRC'), crcErr)
|
|
204
|
+
|
|
205
|
+
const addrClient = mk({ badAddr: true })
|
|
206
|
+
await addrClient.open()
|
|
207
|
+
let addrErr = null
|
|
208
|
+
await addrClient.readHoldingRegisters(0, 1).catch(e => (addrErr = e.message))
|
|
209
|
+
check('RTU 从站地址不匹配 → 报错', addrErr?.includes('从站地址不匹配'), addrErr)
|
|
210
|
+
|
|
211
|
+
const silentClient = mk({ silent: true })
|
|
212
|
+
await silentClient.open()
|
|
213
|
+
const t0 = Date.now()
|
|
214
|
+
let timeoutErr = null
|
|
215
|
+
await silentClient.readHoldingRegisters(0, 1).catch(e => (timeoutErr = e.message))
|
|
216
|
+
check('RTU 无应答 → 硬超时(250ms)', timeoutErr?.includes('超时') && Date.now() - t0 < 1500, timeoutErr)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log('\n━━━ 3. 驱动契约(假端口,免硬件) ━━━')
|
|
220
|
+
{
|
|
221
|
+
// 内存 Modbus 从站:保持寄存器阵列 + 写镜像
|
|
222
|
+
function makeFakeRtu() {
|
|
223
|
+
const holding = new Map()
|
|
224
|
+
const input = new Map()
|
|
225
|
+
const client = {
|
|
226
|
+
isOpen: true,
|
|
227
|
+
closed: false,
|
|
228
|
+
setID() {},
|
|
229
|
+
on() {},
|
|
230
|
+
async readHoldingRegisters(offset, length) {
|
|
231
|
+
const out = []
|
|
232
|
+
for (let i = 0; i < length; i++) out.push(holding.get(offset + i) ?? 0)
|
|
233
|
+
return { data: out }
|
|
234
|
+
},
|
|
235
|
+
async readInputRegisters(offset, length) {
|
|
236
|
+
const out = []
|
|
237
|
+
for (let i = 0; i < length; i++) out.push(input.get(offset + i) ?? 0)
|
|
238
|
+
return { data: out }
|
|
239
|
+
},
|
|
240
|
+
async writeRegister(offset, value) { holding.set(offset, value & 0xFFFF) },
|
|
241
|
+
async writeRegisters(offset, words) { words.forEach((w, i) => holding.set(offset + i, w & 0xFFFF)) },
|
|
242
|
+
async close() {
|
|
243
|
+
client.closed = true
|
|
244
|
+
client.isOpen = false
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
client.__holding = holding
|
|
248
|
+
client.__input = input
|
|
249
|
+
return client
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// float32 26.5(0x41D40000)按 big 端写入保持寄存器 0
|
|
253
|
+
const seedWords = (v) => {
|
|
254
|
+
const b = Buffer.alloc(4)
|
|
255
|
+
b.writeFloatBE(v, 0)
|
|
256
|
+
return [(b[0] << 8) | b[1], (b[2] << 8) | b[3]]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const fake = makeFakeRtu()
|
|
260
|
+
const w = seedWords(26.5)
|
|
261
|
+
fake.__holding.set(0, w[0])
|
|
262
|
+
fake.__holding.set(1, w[1])
|
|
263
|
+
|
|
264
|
+
const linePort = {
|
|
265
|
+
aliveRet: true,
|
|
266
|
+
alive() { return this.aliveRet },
|
|
267
|
+
latestRet: { text: '25.3', at: Date.now() },
|
|
268
|
+
latest() { return this.latestRet },
|
|
269
|
+
written: [],
|
|
270
|
+
async writeLine(line) { this.written.push(line) },
|
|
271
|
+
async close() {},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const factory = createSerialDrivers({
|
|
275
|
+
openRtu: async () => fake,
|
|
276
|
+
openLine: async () => linePort,
|
|
277
|
+
isAvailable: () => true,
|
|
278
|
+
log: { warn: () => {} },
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const cfgRtu = { mode: 'modbus-rtu', path: 'COM9', baudRate: 19200, register: 40001, dataType: 'float32', byteOrder: 'big', scale: 1 }
|
|
282
|
+
const v1 = await factory.readDriver.sample({ driverConfig: cfgRtu })
|
|
283
|
+
check('sample: RTU float32 读数', approx(v1, 26.5), `got ${v1}`)
|
|
284
|
+
|
|
285
|
+
const cfgRtuScaled = { ...cfgRtu, scale: 0.1 }
|
|
286
|
+
const v2 = await factory.readDriver.sample({ driverConfig: cfgRtuScaled })
|
|
287
|
+
check('sample: 缩放系数生效(26.5×0.1)', approx(v2, 2.65), `got ${v2}`)
|
|
288
|
+
|
|
289
|
+
const cfgInput = { ...cfgRtu, registerType: 'input', register: 30001 }
|
|
290
|
+
fake.__input.set(0, 100)
|
|
291
|
+
const v3 = await factory.readDriver.sample({ driverConfig: { ...cfgInput, dataType: 'uint16' } })
|
|
292
|
+
check('sample: 输入寄存器(3x)FC04', v3 === 100, `got ${v3}`)
|
|
293
|
+
|
|
294
|
+
const cfgAscii = { mode: 'ascii-line', path: 'COM10', baudRate: 9600 }
|
|
295
|
+
const v4 = await factory.readDriver.sample({ driverConfig: cfgAscii })
|
|
296
|
+
check('sample: ASCII 行取值', v4 === 25.3, `got ${v4}`)
|
|
297
|
+
|
|
298
|
+
linePort.latestRet = { text: '{"data":{"t":-3.5}}', at: Date.now() }
|
|
299
|
+
const v5 = await factory.readDriver.sample({ driverConfig: { ...cfgAscii, jsonPath: 'data.t' } })
|
|
300
|
+
check('sample: ASCII JSON 路径 + 负值', v5 === -3.5, `got ${v5}`)
|
|
301
|
+
|
|
302
|
+
linePort.latestRet = { text: '99', at: Date.now() - 60_000 }
|
|
303
|
+
const v6 = await factory.readDriver.sample({ driverConfig: { ...cfgAscii, staleMs: 30000 } })
|
|
304
|
+
check('sample: 保鲜窗外 → null 跳帧', v6 === null, `got ${v6}`)
|
|
305
|
+
|
|
306
|
+
// 写 + 回读校验(0~27648 ↔ 0~100 映射)
|
|
307
|
+
const cfgWrite = { mode: 'modbus-rtu', path: 'COM9', baudRate: 19200, register: 40001, dataType: 'uint16', byteOrder: 'big', rawMin: 0, rawMax: 27648, engMin: 0, engMax: 100 }
|
|
308
|
+
const wr1 = await factory.writeDriver.write({ eng: 50, tolerance: 0.5, domain: { min: 0, max: 100 }, driverConfig: cfgWrite })
|
|
309
|
+
check('write: 写+回读一致 ACK', wr1.ok === true && approx(wr1.readback, 50, 0.5), JSON.stringify(wr1))
|
|
310
|
+
|
|
311
|
+
// 从站写入失真(写入值 ≠ 回读值,如从站寄存器故障)→ 回读不一致 → 拒绝 ACK
|
|
312
|
+
const corruptRtu = makeFakeRtu()
|
|
313
|
+
corruptRtu.writeRegister = async function (offset, value) {
|
|
314
|
+
this.__holding.set(offset, (value + 1000) & 0xFFFF)
|
|
315
|
+
}
|
|
316
|
+
corruptRtu.writeRegisters = async function (offset, words) {
|
|
317
|
+
words.forEach((w, i) => this.__holding.set(offset + i, (w + 1000) & 0xFFFF))
|
|
318
|
+
}
|
|
319
|
+
const corruptFactory = createSerialDrivers({
|
|
320
|
+
openRtu: async () => corruptRtu,
|
|
321
|
+
openLine: async () => linePort,
|
|
322
|
+
isAvailable: () => true,
|
|
323
|
+
log: { warn: () => {} },
|
|
324
|
+
})
|
|
325
|
+
const wr2 = await corruptFactory.writeDriver.write({ eng: 50, tolerance: 0.5, domain: { min: 0, max: 100 }, driverConfig: cfgWrite })
|
|
326
|
+
check('write: 回读不一致 → 拒绝', wr2.ok === false && /不一致/.test(wr2.message), JSON.stringify(wr2))
|
|
327
|
+
await corruptFactory.closeAll()
|
|
328
|
+
|
|
329
|
+
// ASCII 写模板下行
|
|
330
|
+
const wr3 = await factory.writeDriver.write({ eng: 42.5, tolerance: 0.5, domain: { min: 0, max: 100 }, driverConfig: { mode: 'ascii-line', path: 'COM10', writeTemplate: 'SET {v}' } })
|
|
331
|
+
check('write: ASCII 模板下行(无回读诚实标注)', wr3.ok === true && linePort.written[0] === 'SET 42.5' && wr3.readback === null, JSON.stringify({ wr3, written: linePort.written }))
|
|
332
|
+
|
|
333
|
+
// 连接测试
|
|
334
|
+
const t1 = await factory.readDriver.test(cfgRtu)
|
|
335
|
+
check('test: 通过并带回读数', t1.ok === true && t1.sampleValue != null, JSON.stringify(t1))
|
|
336
|
+
const t2 = await factory.readDriver.test({ mode: 'modbus-rtu', path: '' })
|
|
337
|
+
check('test: 缺 path 给可读错误', t2.ok === false && /path/.test(t2.message))
|
|
338
|
+
const t3 = await factory.writeDriver.test(cfgWrite)
|
|
339
|
+
check('test: 写驱动测试不触发真实写', t3.ok === true, JSON.stringify(t3))
|
|
340
|
+
|
|
341
|
+
// meta 自描述(前端动态表单数据源)
|
|
342
|
+
check('读驱动 meta.configFields 非空', Array.isArray(factory.readDriver.meta?.configFields) && factory.readDriver.meta.configFields.length >= 10)
|
|
343
|
+
check('写驱动 meta.configFields 含量程映射', factory.writeDriver.meta.configFields.some(f => f.key === 'rawMax'))
|
|
344
|
+
|
|
345
|
+
// 串行队列:前一事务阻塞时后续排队(契约:同一物理链路不并发)
|
|
346
|
+
let busy = 0
|
|
347
|
+
let maxBusy = 0
|
|
348
|
+
const slowFactory = createSerialDrivers({
|
|
349
|
+
openRtu: async () => ({
|
|
350
|
+
isOpen: true,
|
|
351
|
+
setID() {},
|
|
352
|
+
on() {},
|
|
353
|
+
async readHoldingRegisters() {
|
|
354
|
+
busy++
|
|
355
|
+
maxBusy = Math.max(maxBusy, busy)
|
|
356
|
+
await new Promise(r => setTimeout(r, 20))
|
|
357
|
+
busy--
|
|
358
|
+
return { data: [1, 2] }
|
|
359
|
+
},
|
|
360
|
+
async close() {},
|
|
361
|
+
}),
|
|
362
|
+
openLine: async () => linePort,
|
|
363
|
+
isAvailable: () => true,
|
|
364
|
+
log: { warn: () => {} },
|
|
365
|
+
})
|
|
366
|
+
const cfgSlow = { mode: 'modbus-rtu', path: 'COMX', register: 40001 }
|
|
367
|
+
await Promise.all([
|
|
368
|
+
slowFactory.readDriver.sample({ driverConfig: cfgSlow }),
|
|
369
|
+
slowFactory.readDriver.sample({ driverConfig: cfgSlow }),
|
|
370
|
+
slowFactory.readDriver.sample({ driverConfig: cfgSlow }),
|
|
371
|
+
])
|
|
372
|
+
check('同链路事务串行化(无并发读)', maxBusy === 1, `maxBusy=${maxBusy}`)
|
|
373
|
+
|
|
374
|
+
await factory.closeAll()
|
|
375
|
+
check('closeAll 关闭串口连接', fake.closed === true)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log('\n━━━ 3. 插件入口契约(index.mjs + 桩 ctx) ━━━')
|
|
379
|
+
{
|
|
380
|
+
const registered = { daqDrivers: [], dcwDrivers: [], routes: new Map(), tools: [], disposed: [] }
|
|
381
|
+
const ctx = {
|
|
382
|
+
name: 'serial-bridge',
|
|
383
|
+
logger: { info: () => {}, warn: m => console.warn(' [log]', m), error: () => {} },
|
|
384
|
+
daq: {
|
|
385
|
+
registerDriver: d => registered.daqDrivers.push(d),
|
|
386
|
+
nodes: async () => [{ id: 'n1', name: '炉温', driver: 'serial', enabled: true, lineId: 'L1', driverConfig: { path: 'COM3' } }],
|
|
387
|
+
},
|
|
388
|
+
dcw: { registerWriteDriver: d => registered.dcwDrivers.push(d) },
|
|
389
|
+
route: (method, path, handler) => registered.routes.set(`${method} ${path}`, handler),
|
|
390
|
+
omp: { registerTool: t => registered.tools.push(t) },
|
|
391
|
+
onDispose: fn => registered.disposed.push(fn),
|
|
392
|
+
http: {},
|
|
393
|
+
}
|
|
394
|
+
await serialBridge.setup(ctx)
|
|
395
|
+
|
|
396
|
+
check('数采读驱动已注册(kind=serial)', registered.daqDrivers.length === 1 && registered.daqDrivers[0].kind === 'serial')
|
|
397
|
+
check('写控驱动已注册(kind=serial)', registered.dcwDrivers.length === 1 && registered.dcwDrivers[0].kind === 'serial')
|
|
398
|
+
check('驱动 available 探测可用(serialport 已安装)', await registered.daqDrivers[0].available() === true)
|
|
399
|
+
check('API 路由已注册(health/ports/probe)', ['GET /health', 'GET /ports', 'POST /probe'].every(k => registered.routes.has(k)))
|
|
400
|
+
check('omp 工具已注册(serial_ports)', registered.tools.length === 1 && registered.tools[0].name === 'serial_ports')
|
|
401
|
+
check('dispose 已登记(串口回收)', registered.disposed.length >= 1)
|
|
402
|
+
|
|
403
|
+
const health = await registered.routes.get('GET /health')()
|
|
404
|
+
check('GET /health:可用性 + 串口节点识别', health.available === true && health.readNodes.length === 1 && health.readNodes[0].path === 'COM3', JSON.stringify(health))
|
|
405
|
+
const ports = await registered.routes.get('GET /ports')()
|
|
406
|
+
check('GET /ports:系统串口枚举(免硬件)', ports.success === true && Array.isArray(ports.ports), JSON.stringify(ports).slice(0, 120))
|
|
407
|
+
const probe1 = await registered.routes.get('POST /probe')({ awBody: { mode: 'modbus-rtu' } })
|
|
408
|
+
check('POST /probe:缺参数给可读错误', probe1.success === false && /path/i.test(probe1.error))
|
|
409
|
+
const probe2 = await registered.routes.get('POST /probe')({ awBody: { mode: 'ascii-line', path: 'COM_NOT_EXIST_42', timeoutMs: 800 } })
|
|
410
|
+
check('POST /probe:不存在的串口给可行动诊断', probe2.success === true && probe2.ok === false && /串口/.test(probe2.message), JSON.stringify(probe2))
|
|
411
|
+
|
|
412
|
+
const tool = registered.tools[0]
|
|
413
|
+
const r = await tool.handler({})
|
|
414
|
+
check('serial_ports 工具返回诊断文本', r.text.includes('可用性') && r.text.includes('炉温'), r.text)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(`\n━━━ 结果: ${pass} 通过 / ${fail} 失败 ━━━`)
|
|
418
|
+
process.exit(fail ? 1 : 0)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 以 **admin 账号** 登录并给数采 / 数控 / 数字孪生出图。
|
|
3
|
+
* 与 lib.mjs 的视觉走查账号(visual)区分:admin 是文档里写的管理员账号,
|
|
4
|
+
* 能看见全量节点与全部产线;演示工作区由 scripts/ui/seed-admin.mjs 准备。
|
|
5
|
+
*/
|
|
6
|
+
import { launch, openPage, gotoReady } from './lib.mjs'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
|
|
9
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3021'
|
|
10
|
+
const OUT = process.env.AW_OUT ?? '.e2e-shots/admin'
|
|
11
|
+
fs.mkdirSync(OUT, { recursive: true })
|
|
12
|
+
|
|
13
|
+
const loginRes = await fetch(BASE + '/api/users/login', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'content-type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({ email: 'admin@awshop.local', password: 'admin123' }),
|
|
17
|
+
})
|
|
18
|
+
const login = await loginRes.json()
|
|
19
|
+
if (login.code !== 0) throw new Error('admin 登录失败: ' + JSON.stringify(login).slice(0, 200))
|
|
20
|
+
const token = login.data.token
|
|
21
|
+
console.log('登录成功:', login.data.user.name, '(' + login.data.user.role + ')')
|
|
22
|
+
|
|
23
|
+
const browser = await launch()
|
|
24
|
+
const routes = [
|
|
25
|
+
{ path: '/', name: '01-dashboard', wait: 9000 },
|
|
26
|
+
{ path: '/daq', name: '02-daq', wait: 12000 },
|
|
27
|
+
{ path: '/dcw', name: '03-dcw', wait: 9000 },
|
|
28
|
+
{ path: '/town', name: '04-town', wait: 16000 },
|
|
29
|
+
{ path: '/monitor', name: '05-monitor', wait: 7000 },
|
|
30
|
+
]
|
|
31
|
+
for (const r of routes) {
|
|
32
|
+
const page = await openPage(browser, { token, dark: true, width: 1440, height: 900, deviceScaleFactor: 2 })
|
|
33
|
+
const errs = []
|
|
34
|
+
page.on('pageerror', e => errs.push(String(e.message).slice(0, 120)))
|
|
35
|
+
await gotoReady(page, r.path, { wait: r.wait })
|
|
36
|
+
const file = OUT + '/' + r.name + '.png'
|
|
37
|
+
await page.screenshot({ path: file })
|
|
38
|
+
const info = await page.evaluate(() => ({
|
|
39
|
+
nodes: document.querySelectorAll('*').length,
|
|
40
|
+
text: document.body.innerText.replace(/\s+/g, ' ').slice(0, 120),
|
|
41
|
+
}))
|
|
42
|
+
console.log(' ok ' + r.name.padEnd(16) + ' dom=' + String(info.nodes).padStart(6) + ' errs=' + errs.length)
|
|
43
|
+
await page.close()
|
|
44
|
+
}
|
|
45
|
+
await browser.close()
|
|
46
|
+
console.log('->', OUT)
|