agentworkshop 0.7.41 → 0.7.44
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/B8uy9iRW.js +1 -0
- package/.output/public/_nuxt/BGTYYQH-.js +1 -0
- package/.output/public/_nuxt/BUmHEJ77.js +2 -0
- package/.output/public/_nuxt/BY7YrV_D.js +1 -0
- package/.output/public/_nuxt/Bqcgz95s.js +18 -0
- package/.output/public/_nuxt/C7UlqPeD.js +1 -0
- package/.output/public/_nuxt/CTmDn-YP.js +1 -0
- package/.output/public/_nuxt/Cs8pVeVG.js +1 -0
- package/.output/public/_nuxt/DKySmNPI.js +14 -0
- package/.output/public/_nuxt/Di0NY_ht.js +1 -0
- package/.output/public/_nuxt/Du9v6qGB.js +1 -0
- package/.output/public/_nuxt/{DGbd-UiG.js → EHlEMRii.js} +1 -1
- package/.output/public/_nuxt/Eke1fs-U.js +1 -0
- package/.output/public/_nuxt/{DeJykBFf.js → H0BcqZtT.js} +1 -1
- package/.output/public/_nuxt/{C4oW1HsE.js → HSYEkkce.js} +1 -1
- package/.output/public/_nuxt/{D_OTvQza.js → J9oU00D1.js} +1 -1
- package/.output/public/_nuxt/OmpTerminalPanel.BxR5-lvi.css +1 -0
- package/.output/public/_nuxt/SSyAPaHc.js +1 -0
- package/.output/public/_nuxt/TownView.li4EMQxK.css +1 -0
- package/.output/public/_nuxt/VdLk4jLE2.js +1 -0
- package/.output/public/_nuxt/_id_.BhsXoEuk.css +1 -0
- package/.output/public/_nuxt/_id_.DQyj-WKN.css +1 -0
- package/.output/public/_nuxt/{_wsId_.B4WiqkIS.css → _wsId_.CTx-BlfF.css} +1 -1
- package/.output/public/_nuxt/aShsv_rG.js +19 -0
- package/.output/public/_nuxt/{yF0aYMF1.js → aesgXKSr.js} +3 -3
- package/.output/public/_nuxt/agents.B21nUEUX.css +1 -0
- package/.output/public/_nuxt/aml.ekTjxFkf.css +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/b5506ef0-f48e-4f14-a6c0-d65e505d0b09.json +1 -0
- package/.output/public/_nuxt/channel-templates.DplM1pJ3.css +1 -0
- package/.output/public/_nuxt/daq.BdVmoIfe.css +1 -0
- package/.output/public/_nuxt/dcw.DCgyzpWV.css +1 -0
- package/.output/public/_nuxt/{default.J32SgqFY.css → default.Bh3L2HU8.css} +1 -1
- package/.output/public/_nuxt/lHAxdSsj.js +1 -0
- package/.output/public/_nuxt/pages.j9rzRocr.css +1 -0
- package/.output/public/_nuxt/plugins.DhiZbHU8.css +1 -0
- package/.output/public/_nuxt/settings.BRx9rIDg.css +1 -0
- package/.output/public/_nuxt/teams.BLlOPB_Z.css +1 -0
- package/.output/public/_nuxt/tokens.De8xzszn.css +1 -0
- package/.output/public/_nuxt/workshop.D1jFNkrn.css +1 -0
- package/.output/server/chunks/_/nitro.mjs +748 -733
- package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs → OmpTerminalPanel-CVvxpsF7.mjs} +15 -15
- package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs.map → OmpTerminalPanel-CVvxpsF7.mjs.map} +1 -1
- package/.output/server/chunks/build/{TownView--otsnr_7.mjs → TownView-CJ6IsDCc.mjs} +226 -225
- package/.output/server/chunks/build/{TownView--otsnr_7.mjs.map → TownView-CJ6IsDCc.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-Brv-JSvr.mjs → _id_-CVD_H0ir.mjs} +143 -142
- package/.output/server/chunks/build/_id_-CVD_H0ir.mjs.map +1 -0
- package/.output/server/chunks/build/{_id_-D9pl1OZy.mjs → _id_-CtxMb3qp.mjs} +41 -41
- package/.output/server/chunks/build/_id_-CtxMb3qp.mjs.map +1 -0
- package/.output/server/chunks/build/{_wsId_-BASfDf0s.mjs → _wsId_-DpY3T-md.mjs} +514 -444
- package/.output/server/chunks/build/{_wsId_-BASfDf0s.mjs.map → _wsId_-DpY3T-md.mjs.map} +1 -1
- package/.output/server/chunks/build/{agents-CLQqJF2y.mjs → agents-DsCkuXDV.mjs} +49 -46
- package/.output/server/chunks/build/agents-DsCkuXDV.mjs.map +1 -0
- package/.output/server/chunks/build/{aml-_7FdFL2p.mjs → aml-CT0xKGKI.mjs} +122 -105
- package/.output/server/chunks/build/aml-CT0xKGKI.mjs.map +1 -0
- package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs +117 -0
- package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs.map +1 -0
- package/.output/server/chunks/build/{channel-templates-p6SZNjlM.mjs → channel-templates-Dla9FbYa.mjs} +49 -37
- package/.output/server/chunks/build/channel-templates-Dla9FbYa.mjs.map +1 -0
- package/.output/server/chunks/build/{daq-DkMvD8sg.mjs → daq-DWWSKGAV.mjs} +89 -88
- package/.output/server/chunks/build/daq-DWWSKGAV.mjs.map +1 -0
- package/.output/server/chunks/build/{dcw-C1jgVJkO.mjs → dcw-CT8k17_E.mjs} +44 -43
- package/.output/server/chunks/build/dcw-CT8k17_E.mjs.map +1 -0
- package/.output/server/chunks/build/{default-BE4r0FZN.mjs → default-D3525UFX.mjs} +16 -15
- package/.output/server/chunks/build/{default-BE4r0FZN.mjs.map → default-D3525UFX.mjs.map} +1 -1
- package/.output/server/chunks/build/{monitor-C0okQaXz.mjs → monitor-Dh2Kvaq5.mjs} +2 -2
- package/.output/server/chunks/build/{monitor-C0okQaXz.mjs.map → monitor-Dh2Kvaq5.mjs.map} +1 -1
- package/.output/server/chunks/build/{pages-B0bUWnC6.mjs → pages-BHcSBM2D.mjs} +24 -18
- package/.output/server/chunks/build/pages-BHcSBM2D.mjs.map +1 -0
- package/.output/server/chunks/build/{plugins-lGlUr1lj.mjs → plugins-BNgIi61W.mjs} +23 -17
- package/.output/server/chunks/build/plugins-BNgIi61W.mjs.map +1 -0
- package/.output/server/chunks/build/{settings-Ddd1qx2Z.mjs → settings-ipYBDWdn.mjs} +45 -44
- package/.output/server/chunks/build/settings-ipYBDWdn.mjs.map +1 -0
- package/.output/server/chunks/build/{teams-MEDLZdMn.mjs → teams-5lx-CwFa.mjs} +47 -44
- package/.output/server/chunks/build/teams-5lx-CwFa.mjs.map +1 -0
- package/.output/server/chunks/build/{tokens-C7fTM5C2.mjs → tokens-q-IDT8Io.mjs} +73 -53
- package/.output/server/chunks/build/tokens-q-IDT8Io.mjs.map +1 -0
- package/.output/server/chunks/build/{town-CoQdblbZ.mjs → town-QJWB5wng.mjs} +3 -2
- package/.output/server/chunks/build/town-QJWB5wng.mjs.map +1 -0
- package/.output/server/chunks/build/{workshop-BL5bhfs0.mjs → workshop-BIlCj_ZP.mjs} +45 -45
- package/.output/server/chunks/build/workshop-BIlCj_ZP.mjs.map +1 -0
- package/.output/server/chunks/routes/renderer.mjs +1 -1
- package/.output/server/chunks/virtual/entry.mjs +637 -86
- package/.output/server/chunks/virtual/entry.mjs.map +1 -1
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README-zh.md +13 -3
- package/README.md +13 -3
- package/app/components/AppHeader.vue +961 -959
- package/app/components/workshop/A2aRpcDebugger.vue +237 -237
- package/app/components/workshop/AgentInspectorDrawer.vue +424 -424
- package/app/components/workshop/AgentLanesView.vue +998 -998
- package/app/components/workshop/ArtifactCard.vue +120 -120
- package/app/components/workshop/AssetLibrary.vue +452 -452
- package/app/components/workshop/ChannelSessionList.vue +812 -804
- package/app/components/workshop/CommandPalette.vue +269 -269
- package/app/components/workshop/Composer.vue +827 -827
- package/app/components/workshop/DeviceTwinPanel.vue +643 -643
- package/app/components/workshop/EventBlock.vue +602 -602
- package/app/components/workshop/FileSelectorModal.vue +188 -188
- package/app/components/workshop/InspectorPanel.vue +301 -301
- package/app/components/workshop/MemoryPanel.vue +364 -364
- package/app/components/workshop/MultiChannelView.vue +189 -189
- package/app/components/workshop/TaskBoardView.vue +686 -686
- package/app/components/workshop/TaskInspectorDrawer.vue +390 -390
- package/app/components/workshop/TranscriptTimeline.vue +601 -601
- package/app/components/workshop/blocks/ClusterArtifact.vue +61 -61
- package/app/components/workshop/blocks/ClusterError.vue +85 -85
- package/app/components/workshop/blocks/ClusterLife.vue +87 -87
- package/app/components/workshop/blocks/ClusterMember.vue +95 -93
- package/app/components/workshop/blocks/ClusterMemory.vue +84 -84
- package/app/components/workshop/blocks/ClusterOther.vue +67 -67
- package/app/components/workshop/blocks/ClusterStatus.vue +149 -147
- package/app/components/workshop/blocks/ClusterTool.vue +158 -158
- package/app/components/workshop/terminal/OmpTerminalPanel.vue +1011 -1011
- package/app/components/workshop/town/TownView.vue +56 -56
- package/app/middleware/locale-cookie.global.ts +22 -0
- package/app/pages/aml/index.vue +24 -2
- package/app/pages/daq/[id].vue +11 -11
- package/app/pages/daq/index.vue +2316 -2316
- package/app/pages/dcw/[id].vue +18 -19
- package/app/pages/dcw/index.vue +3 -3
- package/app/pages/index.vue +987 -981
- package/app/pages/plugins/index.vue +317 -311
- package/app/pages/settings.vue +1564 -1562
- package/app/pages/tokens.vue +15 -15
- package/app/pages/workshop/agents.vue +612 -609
- package/app/pages/workshop/channel-templates.vue +18 -11
- package/app/pages/workshop/index.vue +668 -668
- package/app/pages/workshop/teams.vue +743 -740
- package/app/pages/workshop/w/[wsId].vue +704 -704
- package/app/plugins/locale-restore.client.ts +16 -1
- package/app/utils/builtin-catalog.ts +104 -0
- package/config.yml +170 -170
- package/docs/full-test-plan.md +2 -3
- package/i18n/locales/en.ts +293 -35
- package/i18n/locales/zh-CN.ts +265 -7
- package/package.json +2 -2
- package/scripts/_dbg-perms-e2e.mjs +132 -109
- package/scripts/_demo-agentteam-capture.mjs +278 -0
- package/scripts/_e2e-ui-functional.mjs +371 -0
- package/scripts/_i18n-en-cjk-scan.mjs +28 -0
- package/scripts/_i18n-en-verify.mjs +107 -0
- package/scripts/_i18n-extra-checks.mjs +53 -0
- package/scripts/_i18n-scan-hardcoded.mjs +180 -0
- package/scripts/_i18n-verify-keys.mjs +50 -0
- package/scripts/_loop-biax-campaign.mjs +15 -9
- package/scripts/_sweep-button-contrast.mjs +87 -0
- package/scripts/ui/record-demo.mjs +557 -0
- package/server/plugins-builtin/diag-bridge/index.mjs +608 -661
- package/server/plugins-builtin/rag-bridge/index.mjs +57 -133
- package/server/services/workshop/daq/drivers.ts +11 -0
- package/.output/public/_nuxt/88GOowgO2.js +0 -1
- package/.output/public/_nuxt/BN16aRdQ.js +0 -1
- package/.output/public/_nuxt/BWVhZntO.js +0 -1
- package/.output/public/_nuxt/BXxH0UQE.js +0 -1
- package/.output/public/_nuxt/BsROmY89.js +0 -1
- package/.output/public/_nuxt/C0ZrS1KL.js +0 -19
- package/.output/public/_nuxt/C4s_niy-.js +0 -14
- package/.output/public/_nuxt/D6mbFA-a.js +0 -1
- package/.output/public/_nuxt/DS8qj8EL.js +0 -1
- package/.output/public/_nuxt/DUjW5Yq5.js +0 -1
- package/.output/public/_nuxt/Dap5PmTL.js +0 -2
- package/.output/public/_nuxt/Dqw1lpr-.js +0 -1
- package/.output/public/_nuxt/DvLxILQP.js +0 -1
- package/.output/public/_nuxt/OmpTerminalPanel.B_Pl5Ynm.css +0 -1
- package/.output/public/_nuxt/TownView.f-YOdXRs.css +0 -1
- package/.output/public/_nuxt/_id_.D9ymtHB5.css +0 -1
- package/.output/public/_nuxt/_id_.jf-_8E_c.css +0 -1
- package/.output/public/_nuxt/agents.ZD0rVdHE.css +0 -1
- package/.output/public/_nuxt/aml.BN8ZEW4h.css +0 -1
- package/.output/public/_nuxt/builds/meta/78a295bb-2072-48a4-8b46-6d545fdae743.json +0 -1
- package/.output/public/_nuxt/channel-templates.BOtahzPi.css +0 -1
- package/.output/public/_nuxt/daq.DjSgY94l.css +0 -1
- package/.output/public/_nuxt/dcw.8Y7FqCVN.css +0 -1
- package/.output/public/_nuxt/eqq3wWQF.js +0 -18
- package/.output/public/_nuxt/pages.D7aSDlUI.css +0 -1
- package/.output/public/_nuxt/plugins.sb4nDGqr.css +0 -1
- package/.output/public/_nuxt/qr6R6Ha1.js +0 -1
- package/.output/public/_nuxt/settings.BTgR-NiE.css +0 -1
- package/.output/public/_nuxt/teams.CxnxKVVs.css +0 -1
- package/.output/public/_nuxt/tokens.BFY7qv4f.css +0 -1
- package/.output/public/_nuxt/workshop.CR_QzeAN.css +0 -1
- package/.output/server/chunks/build/_id_-Brv-JSvr.mjs.map +0 -1
- package/.output/server/chunks/build/_id_-D9pl1OZy.mjs.map +0 -1
- package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +0 -1
- package/.output/server/chunks/build/aml-_7FdFL2p.mjs.map +0 -1
- package/.output/server/chunks/build/channel-templates-p6SZNjlM.mjs.map +0 -1
- package/.output/server/chunks/build/daq-DkMvD8sg.mjs.map +0 -1
- package/.output/server/chunks/build/dcw-C1jgVJkO.mjs.map +0 -1
- package/.output/server/chunks/build/pages-B0bUWnC6.mjs.map +0 -1
- package/.output/server/chunks/build/plugins-lGlUr1lj.mjs.map +0 -1
- package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +0 -1
- package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +0 -1
- package/.output/server/chunks/build/tokens-C7fTM5C2.mjs.map +0 -1
- package/.output/server/chunks/build/town-CoQdblbZ.mjs.map +0 -1
- package/.output/server/chunks/build/workshop-BL5bhfs0.mjs.map +0 -1
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +0 -1
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +0 -1
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +0 -153
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +0 -153
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +0 -431
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +0 -180
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +0 -112
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +0 -227
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +0 -131
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +0 -160
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +0 -157
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +0 -348
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +0 -296
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +0 -285
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +0 -217
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +0 -301
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +0 -151
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +0 -135
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +0 -156
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +0 -72
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +0 -469
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +0 -259
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +0 -97
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +0 -277
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +0 -171
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +0 -425
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +0 -99
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +0 -325
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +0 -332
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +0 -43
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +0 -154
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +0 -531
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +0 -439
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +0 -323
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +0 -433
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +0 -280
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +0 -117
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +0 -160
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +0 -101
- package/scripts/.mimosa/hook-status/sess_aeaecb58-8833-4b83-b93c-050467d21e03-d944d71bb3.json +0 -14
- package/scripts/_dbg-audit-all-pages.mjs +0 -43
- package/scripts/_dbg-daq-frames-e2e.mjs +0 -118
- package/scripts/_dbg-daq-rowstyle-verify.mjs +0 -112
- package/scripts/_dbg-hitl-timeout-e2e.mjs +0 -93
- package/scripts/_dbg-live-line-e2e.mjs +0 -279
- package/scripts/_dbg-minimal-omp.mjs +0 -77
- package/scripts/_dbg-mode-nav.mjs +0 -173
- package/scripts/_dbg-real-scenario-timeline.log +0 -55
- package/scripts/_dbg-team-repro.mjs +0 -76
- package/scripts/_dbg-town-agents-probe.mjs +0 -60
- package/scripts/_dbg-visual-acceptance.mjs +0 -63
- package/scripts/_townview_style.css +0 -1185
- package/scripts/_townview_template.html +0 -1215
- package/server/data/agent-node-bindings.json +0 -554
- package/server/data/daq-templates.json +0 -16
- package/server/data/daqs.json +0 -7464
- package/server/data/dcw-lines.json +0 -1787
- package/server/data/dcw-products.json +0 -1626
- package/server/data/dcw-recipes.json +0 -4672
- package/server/data/dcw-rollback.json +0 -20754
- package/server/data/dcw-runs.json +0 -6354
- package/server/data/dcw-templates.json +0 -146
- package/server/data/dcw-writes.json +0 -15650
- package/server/data/dcws.json +0 -9033
- package/server/data/device-twins.json +0 -133
- package/server/data/line-runs.json +0 -322
- package/server/data/scene-layouts.json +0 -277
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AML 端到端测试(对活服):fixtures → 数据集构建 → 隔离拒绝 → 训练作业 → 门禁 →
|
|
3
|
-
* 排行榜 → 晋升守卫 → 预测守卫 → 审计归属。
|
|
4
|
-
*
|
|
5
|
-
* 运行:
|
|
6
|
-
* 1) 存根模式(无 Python 环境;服务器以 AML_STUB=1 启动):
|
|
7
|
-
* npx tsx --tsconfig .nuxt/tsconfig.server.json scripts/e2e-aml.ts
|
|
8
|
-
* 2) 真实模式(服务器正常启动,venv 供给可用):… scripts/e2e-aml.ts --real
|
|
9
|
-
* 真实模式走 venv 供给 + torch 训练 + ONNX 评估(首次约数分钟)。
|
|
10
|
-
*
|
|
11
|
-
* 前置:AW_BASE(默认 http://127.0.0.1:3001,仅允许本机回环);AW_E2E_TOKEN
|
|
12
|
-
* (admin/editor token —— 建数采节点需要 editor+;缺省时注册临时用户,
|
|
13
|
-
* 仅 fresh 库首位=admin 时可行)。
|
|
14
|
-
*/
|
|
15
|
-
import { readFileSync } from 'node:fs'
|
|
16
|
-
import { join } from 'node:path'
|
|
17
|
-
|
|
18
|
-
const BASE_URL = new URL(process.env.AW_BASE ?? 'http://127.0.0.1:3001')
|
|
19
|
-
// 测试脚本安全守卫:仅允许 http(s) + 本机回环/私网主机名(防误把测试流量打向外部)
|
|
20
|
-
if (BASE_URL.protocol !== 'http:' && BASE_URL.protocol !== 'https:') {
|
|
21
|
-
console.error('AW_BASE 协议必须为 http/https')
|
|
22
|
-
process.exit(1)
|
|
23
|
-
}
|
|
24
|
-
const host = BASE_URL.hostname
|
|
25
|
-
const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.endsWith('.local') || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host)
|
|
26
|
-
if (!isLocal && process.env.AW_ALLOW_REMOTE_BASE !== '1') {
|
|
27
|
-
console.error(`AW_BASE 主机 ${host} 非本机/私网;确需远程测试请设 AW_ALLOW_REMOTE_BASE=1`)
|
|
28
|
-
process.exit(1)
|
|
29
|
-
}
|
|
30
|
-
const BASE = BASE_URL.origin
|
|
31
|
-
const REAL = process.argv.includes('--real')
|
|
32
|
-
|
|
33
|
-
let passed = 0
|
|
34
|
-
let failures = 0
|
|
35
|
-
let step = 0
|
|
36
|
-
function check(name: string, ok: boolean, detail = ''): void {
|
|
37
|
-
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
38
|
-
if (ok) passed++
|
|
39
|
-
else failures++
|
|
40
|
-
}
|
|
41
|
-
function section(title: string): void {
|
|
42
|
-
step += 1
|
|
43
|
-
console.log(`\n━━━ ${step}. ${title} ━━━`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/* eslint-disable @typescript-eslint/no-explicit-any -- e2e 断言脚本:响应体按动态 JSON 探查 */
|
|
47
|
-
|
|
48
|
-
interface ApiRes { status: number, ok: boolean, json: any }
|
|
49
|
-
|
|
50
|
-
async function api(method: string, path: string, opts: { body?: unknown, token?: string } = {}): Promise<ApiRes> {
|
|
51
|
-
const res = await fetch(`${BASE}${path}`, {
|
|
52
|
-
method,
|
|
53
|
-
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${opts.token ?? ''}` },
|
|
54
|
-
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
55
|
-
})
|
|
56
|
-
const json = await res.json().catch(() => null)
|
|
57
|
-
return { status: res.status, ok: res.ok, json }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------- 登录 ----------
|
|
61
|
-
let token = process.env.AW_E2E_TOKEN ?? ''
|
|
62
|
-
if (!token) {
|
|
63
|
-
const name = `e2e-aml-${process.pid.toString(36)}-${Date.now().toString(36).slice(-5)}`
|
|
64
|
-
const reg = await api('POST', '/api/workshop/users/register', { body: { name }, token: '' })
|
|
65
|
-
token = reg.json?.data?.token ?? ''
|
|
66
|
-
}
|
|
67
|
-
if (!token) {
|
|
68
|
-
console.error('缺少 token:设置 AW_E2E_TOKEN(admin/editor)后重试')
|
|
69
|
-
process.exit(1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const suffix = `${process.pid.toString(36)}${Date.now().toString(36).slice(-5)}`
|
|
73
|
-
const STUB_TRAIN_PY = [
|
|
74
|
-
'# e2e 存根训练代码(AML_STUB=1 时不会真正执行)',
|
|
75
|
-
'import amlkit',
|
|
76
|
-
'amlkit.report_progress(50, \'stub\')',
|
|
77
|
-
'amlkit.save_metrics({\'agent\': {\'note\': \'stub\'}})',
|
|
78
|
-
'print(\'stub train done\')',
|
|
79
|
-
'',
|
|
80
|
-
].join('\n')
|
|
81
|
-
|
|
82
|
-
async function main(): Promise<void> {
|
|
83
|
-
section('平台概览')
|
|
84
|
-
const overview = await api('GET', '/api/workshop/aml', { token })
|
|
85
|
-
check('GET /aml 返回运行时状态', overview.ok && !!overview.json?.data?.runtime, `python.ok=${overview.json?.data?.runtime?.python?.ok}`)
|
|
86
|
-
if (overview.json?.data?.runtime?.python?.ok === false && REAL) {
|
|
87
|
-
console.error('真实模式要求服务器具备 Python;当前探测失败:', overview.json?.data?.runtime?.python?.reason)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
section('门禁阈值放宽(幂等;无权限则降级;测试后恢复)')
|
|
91
|
-
const keysToRelax = ['aml.gates.minRows', 'aml.gates.minRuns', 'aml.gates.nrmse', 'aml.gates.rolloutNrmse', 'aml.gates.valTestGap'] as const
|
|
92
|
-
const relaxedValues: Record<string, unknown> = {
|
|
93
|
-
'aml.gates.minRows': 50,
|
|
94
|
-
'aml.gates.minRuns': 2,
|
|
95
|
-
'aml.gates.nrmse': 0.99,
|
|
96
|
-
'aml.gates.rolloutNrmse': 0.99,
|
|
97
|
-
'aml.gates.valTestGap': 2,
|
|
98
|
-
}
|
|
99
|
-
const before = await api('GET', '/api/system/settings', { token })
|
|
100
|
-
const prevEffective: Record<string, unknown> = before.json?.data?.effective ?? {}
|
|
101
|
-
const patch = await api('PATCH', '/api/system/settings', { token, body: { overrides: relaxedValues } })
|
|
102
|
-
const gatesRelaxed = patch.ok
|
|
103
|
-
const restoreSettings = async (): Promise<void> => {
|
|
104
|
-
if (!gatesRelaxed) return
|
|
105
|
-
const restore: Record<string, unknown> = {}
|
|
106
|
-
for (const k of keysToRelax) {
|
|
107
|
-
if (k in prevEffective) restore[k] = prevEffective[k]
|
|
108
|
-
}
|
|
109
|
-
if (Object.keys(restore).length > 0) await api('PATCH', '/api/system/settings', { token, body: { overrides: restore } })
|
|
110
|
-
}
|
|
111
|
-
check('门禁阈值可写(权限或键缺失则降级)', gatesRelaxed || patch.status === 403 || patch.status === 404 || patch.status === 401, `status=${patch.status}`)
|
|
112
|
-
|
|
113
|
-
section('产线 fixtures')
|
|
114
|
-
const line = await api('POST', '/api/workshop/dcw/lines', { token, body: { name: `AML-E2E 线 ${suffix}` } })
|
|
115
|
-
const lineId: string = line.json?.data?.line?.id
|
|
116
|
-
check('创建产线', line.ok && !!lineId, lineId ?? line.json?.message)
|
|
117
|
-
const product = await api('POST', '/api/workshop/dcw/products', { token, body: { lineId, name: `AML-E2E 产品 ${suffix}` } })
|
|
118
|
-
const productId: string = product.json?.data?.product?.id
|
|
119
|
-
check('创建产品', product.ok && !!productId, productId ?? product.json?.message)
|
|
120
|
-
const dcwNode = await api('POST', '/api/workshop/dcw', {
|
|
121
|
-
token,
|
|
122
|
-
body: { name: `aml-e2e-dcw-${suffix}`, driver: 'mock', lineId, templateRef: 'temp-sp' },
|
|
123
|
-
})
|
|
124
|
-
const dcwNodeId: string = dcwNode.json?.data?.node?.id
|
|
125
|
-
check('创建数控节点(mock)', dcwNode.ok && !!dcwNodeId, dcwNodeId ?? dcwNode.json?.message)
|
|
126
|
-
const recipe = await api('POST', '/api/workshop/dcw/recipes', {
|
|
127
|
-
token,
|
|
128
|
-
body: {
|
|
129
|
-
productId, name: `AML-E2E 配方 ${suffix}`, description: 'e2e',
|
|
130
|
-
params: [{ nodeId: dcwNodeId, value: 60, min: 0, max: 100 }],
|
|
131
|
-
},
|
|
132
|
-
})
|
|
133
|
-
const recipeId: string = recipe.json?.data?.recipe?.id
|
|
134
|
-
check('创建配方(节点级绑定)', recipe.ok && !!recipeId, recipeId ?? recipe.json?.message)
|
|
135
|
-
|
|
136
|
-
section('数采节点 ×2')
|
|
137
|
-
const mkDaq = async (name: string, templateRef: string) => api('POST', '/api/workshop/daq', {
|
|
138
|
-
token,
|
|
139
|
-
body: { name, templateRef, driver: 'mock', lineId, intervalMs: 1000, publishIntervalMs: 0 },
|
|
140
|
-
})
|
|
141
|
-
const daqCtrl = await mkDaq(`aml-e2e-ctrl-${suffix}`, 'daq-temp-tc')
|
|
142
|
-
const daqTmp = await mkDaq(`aml-e2e-tmp-${suffix}`, 'daq-pressure-tx')
|
|
143
|
-
const ctrlId: string = daqCtrl.json?.data?.node?.id
|
|
144
|
-
const tmpId: string = daqTmp.json?.data?.node?.id
|
|
145
|
-
check('数采节点(控制量)', daqCtrl.ok, ctrlId)
|
|
146
|
-
check('数采节点(目标量)', daqTmp.ok, tmpId)
|
|
147
|
-
|
|
148
|
-
section('开跑 4 个批次(短窗采集;样本按 run 打标)')
|
|
149
|
-
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))
|
|
150
|
-
for (let i = 0; i < 4; i++) {
|
|
151
|
-
const start = await api('POST', `/api/workshop/dcw/lines/${lineId}/start`, { token, body: { recipeId } })
|
|
152
|
-
if (!start.ok) {
|
|
153
|
-
check(`批次 ${i + 1} 开跑`, false, JSON.stringify(start.json)?.slice(0, 120))
|
|
154
|
-
break
|
|
155
|
-
}
|
|
156
|
-
await sleep(20000)
|
|
157
|
-
await api('POST', `/api/workshop/dcw/lines/${lineId}/stop`, { token, body: {} })
|
|
158
|
-
check(`批次 ${i + 1} 采集完成(20s)`, true)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
section('数据集构建(隔离三元组)')
|
|
162
|
-
const dsSpec = {
|
|
163
|
-
lineId, productId, recipeId,
|
|
164
|
-
nodes: [{ nodeId: ctrlId, role: 'control' }, { nodeId: tmpId, role: 'target' }],
|
|
165
|
-
beatMs: 1000, window: { historySteps: 3, horizonSteps: 2 },
|
|
166
|
-
cleaning: { hampelK: 5, maxInterpMs: 3000, maxDropRatio: 0.5 },
|
|
167
|
-
split: { valRatio: 0.25, testRatio: 0.25, seed: 42 },
|
|
168
|
-
purpose: 'mpc_surrogate',
|
|
169
|
-
note: 'e2e',
|
|
170
|
-
}
|
|
171
|
-
const ds = await api('POST', '/api/workshop/aml/datasets', { token, body: dsSpec })
|
|
172
|
-
const rowCount = ds.json?.data?.dataset?.rowCount ?? 0
|
|
173
|
-
check('数据集构建成功', ds.ok && rowCount > 0, `rows=${rowCount} runs=${ds.json?.data?.dataset?.runIds?.length} ${ds.json?.message ?? ''}`)
|
|
174
|
-
const datasetId: string = ds.json?.data?.dataset?.id
|
|
175
|
-
const dsList = await api('GET', `/api/workshop/aml/datasets?recipeId=${recipeId}`, { token })
|
|
176
|
-
check('数据集列表可见', dsList.ok && (dsList.json?.data?.datasets?.length ?? 0) >= 1)
|
|
177
|
-
|
|
178
|
-
section('隔离硬约束')
|
|
179
|
-
const badLine = await api('POST', '/api/workshop/aml/datasets', { token, body: { ...dsSpec, lineId: 'ln-not-exist' } })
|
|
180
|
-
check('节点产线不一致被拒', !badLine.ok && String(badLine.json?.code ?? '').startsWith('AML_'), `code=${badLine.json?.code}`)
|
|
181
|
-
const badProduct = await api('POST', '/api/workshop/aml/datasets', { token, body: { ...dsSpec, productId: 'pd-not-exist' } })
|
|
182
|
-
check('批次归属不一致被拒', !badProduct.ok, `code=${badProduct.json?.code}`)
|
|
183
|
-
|
|
184
|
-
section('训练作业(提交 → 终态)')
|
|
185
|
-
const code = REAL
|
|
186
|
-
? readFileSync(join(process.cwd(), 'server', 'services', 'workshop', 'aml', 'python', 'train-example.py'), 'utf8')
|
|
187
|
-
: STUB_TRAIN_PY
|
|
188
|
-
const job = await api('POST', '/api/workshop/aml/jobs', {
|
|
189
|
-
token,
|
|
190
|
-
body: { datasetId, code, changeNote: 'e2e 基线', seed: 42, params: REAL ? { epochs: 30 } : {} },
|
|
191
|
-
})
|
|
192
|
-
const jobId: string = job.json?.data?.job?.id
|
|
193
|
-
check('作业提交成功', job.ok, jobId ?? job.json?.message)
|
|
194
|
-
let finalJob: any = null
|
|
195
|
-
const budgetMs = REAL ? 15 * 60_000 : 4 * 60_000
|
|
196
|
-
const t0 = Date.now()
|
|
197
|
-
while (Date.now() - t0 < budgetMs) {
|
|
198
|
-
await sleep(2000)
|
|
199
|
-
const r = await api('GET', `/api/workshop/aml/jobs/${jobId}`, { token })
|
|
200
|
-
finalJob = r.json?.data?.job
|
|
201
|
-
if (finalJob && ['done', 'failed', 'timeout', 'cancelled'].includes(finalJob.status)) break
|
|
202
|
-
}
|
|
203
|
-
check('作业终态到达', !!finalJob, `status=${finalJob?.status} err=${String(finalJob?.error ?? '').slice(0, 140)}`)
|
|
204
|
-
const stubDone = finalJob?.status === 'done'
|
|
205
|
-
if (finalJob) {
|
|
206
|
-
const detail = await api('GET', `/api/workshop/aml/jobs/${jobId}`, { token })
|
|
207
|
-
check('门禁报告产出(逐项可读)', Array.isArray(detail.json?.data?.gates?.checks),
|
|
208
|
-
`passed=${detail.json?.data?.gates?.passed} checks=${detail.json?.data?.gates?.checks?.length}`)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
section('排行榜与实验谱系')
|
|
212
|
-
const lb = await api('GET', `/api/workshop/aml/experiments?datasetId=${datasetId}`, { token })
|
|
213
|
-
check('实验行已登记', lb.ok && (lb.json?.data?.experiments?.length ?? 0) >= 1, `n=${lb.json?.data?.experiments?.length}`)
|
|
214
|
-
const expRow = lb.json?.data?.experiments?.[0]
|
|
215
|
-
check('谱系/改动声明落库', !!expRow && Array.isArray(expRow.gates?.checks) && typeof expRow.changeNote === 'string', `status=${expRow?.status}`)
|
|
216
|
-
|
|
217
|
-
section('模型注册与晋升守卫')
|
|
218
|
-
const models = await api('GET', `/api/workshop/aml/models?recipeId=${recipeId}`, { token })
|
|
219
|
-
const cand = models.json?.data?.models?.find((m: any) => m.stage === 'candidate')
|
|
220
|
-
if (stubDone && cand) {
|
|
221
|
-
check('候选模型已登记(门禁通过)', true, cand.id)
|
|
222
|
-
const promote = await api('POST', `/api/workshop/aml/models/${cand.id}/promote`, { token, body: { toStage: 'production' } })
|
|
223
|
-
if (REAL) {
|
|
224
|
-
// 真实工件:深检(onnxruntime 试推理)应通过 → 晋升成功 → production 在册
|
|
225
|
-
check('真实工件通过深检并晋升 production', promote.ok && promote.json?.data?.ok === true,
|
|
226
|
-
`status=${promote.status} code=${promote.json?.code} ${promote.json?.message ?? ''}`)
|
|
227
|
-
const prods = await api('GET', `/api/workshop/aml/models?recipeId=${recipeId}&stage=production`, { token })
|
|
228
|
-
check('production 模型在册(唯一)', (prods.json?.data?.models?.length ?? 0) === 1,
|
|
229
|
-
`n=${prods.json?.data?.models?.length}`)
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// 存根工件(STUB 标记/无真实 onnx)必须被深检拦下 —— 这是守卫,不是缺陷
|
|
233
|
-
check('无效工件被生产晋升深检拦截', promote.status === 422 && String(promote.json?.code ?? '').includes('ARTIFACT'),
|
|
234
|
-
`status=${promote.status} code=${promote.json?.code}`)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
check('门禁未过 → 注册表无候选模型(干净)', !cand, `models=${models.json?.data?.models?.length}`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
section('预测守卫(未知模型)')
|
|
242
|
-
const noProd = await api('POST', '/api/workshop/aml/models/mdl-not-exist/predict', { token, body: { history: [[0]] } })
|
|
243
|
-
check('未知模型预测 404', noProd.status === 404, `status=${noProd.status}`)
|
|
244
|
-
|
|
245
|
-
section('审计归属(ops_log)')
|
|
246
|
-
const ops = await api('GET', '/api/workshop/ops-logs?limit=300', { token })
|
|
247
|
-
const opsRows: any[] = ops.json?.data?.logs ?? []
|
|
248
|
-
const amlOps = opsRows.filter(r => String(r.action ?? '').startsWith('aml.'))
|
|
249
|
-
check('aml.* 操作已入审计', amlOps.length >= 1, `n=${amlOps.length} actions=${[...new Set(amlOps.map(r => r.action))].slice(0, 5).join(',')}`)
|
|
250
|
-
|
|
251
|
-
console.log(`\n━━━ 结果:${passed} PASS / ${failures} FAIL ━━━`)
|
|
252
|
-
await restoreSettings()
|
|
253
|
-
process.exit(failures > 0 ? 1 : 0)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
main().catch(async (err) => {
|
|
257
|
-
console.error(err)
|
|
258
|
-
process.exit(1)
|
|
259
|
-
})
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"""Harness 可用性 UI E2E:用户名登录 → 仪表盘 Harness 面板(dsh=未安装灰化) → agents 模板选择禁用 → teams 页截图。
|
|
2
|
-
dsh 未安装态由外部脚本预置;恢复也由外部执行(本脚本不发起 HTTP)。
|
|
3
|
-
AW_BASE 环境变量切换目标实例(默认 :3021);EXPECT_OFF=1 时断言恰好 1 条灰化。"""
|
|
4
|
-
import asyncio, os, sys
|
|
5
|
-
from playwright.async_api import async_playwright
|
|
6
|
-
|
|
7
|
-
BASE = os.environ.get('AW_BASE', 'http://127.0.0.1:3021')
|
|
8
|
-
EXPECT_OFF = os.environ.get('EXPECT_OFF', '1') == '1'
|
|
9
|
-
SHOT = '.e2e-shots'
|
|
10
|
-
|
|
11
|
-
async def main():
|
|
12
|
-
results = []
|
|
13
|
-
def ok(cond, name, extra=''):
|
|
14
|
-
results.append((cond, name))
|
|
15
|
-
print((' ✓ ' if cond else ' ✗ ') + name + (f' {extra}' if extra and not cond else ''), flush=True)
|
|
16
|
-
|
|
17
|
-
async with async_playwright() as p:
|
|
18
|
-
browser = await p.chromium.launch()
|
|
19
|
-
page = await browser.new_page(viewport={'width': 1440, 'height': 900})
|
|
20
|
-
|
|
21
|
-
# ---- 1. 用户名登录(admin/admin123;水合时机在 dev 下有抖动 → 重试 3 轮)----
|
|
22
|
-
await page.goto(BASE + '/workshop', wait_until='domcontentloaded', timeout=90000)
|
|
23
|
-
await page.reload(wait_until='domcontentloaded', timeout=90000)
|
|
24
|
-
await page.wait_for_selector('input[placeholder="邮箱 / 用户名"]', timeout=60000)
|
|
25
|
-
login_status = None
|
|
26
|
-
for attempt in range(3):
|
|
27
|
-
await page.wait_for_timeout(2500 + attempt * 2000) # 等 Vue 水合
|
|
28
|
-
await page.fill('input[placeholder="邮箱 / 用户名"]', 'admin')
|
|
29
|
-
await page.fill('.ant-input-password input', 'admin123')
|
|
30
|
-
try:
|
|
31
|
-
async with page.expect_response(lambda r: '/api/users/login' in r.url, timeout=15000) as ri:
|
|
32
|
-
await page.locator('.ant-input-password input').first.press('Enter')
|
|
33
|
-
login_status = (await ri.value).status
|
|
34
|
-
except Exception:
|
|
35
|
-
login_status = None
|
|
36
|
-
login_input = page.locator('input[placeholder="邮箱 / 用户名"]')
|
|
37
|
-
gone = (await login_input.count()) == 0 or not await login_input.first.is_visible()
|
|
38
|
-
if login_status == 200 and gone:
|
|
39
|
-
break
|
|
40
|
-
ok(login_status == 200, f'登录 POST 200(实际 {login_status})')
|
|
41
|
-
login_input = page.locator('input[placeholder="邮箱 / 用户名"]')
|
|
42
|
-
logged = (await login_input.count()) == 0 or not await login_input.first.is_visible()
|
|
43
|
-
ok(logged, '用户名登录进入系统')
|
|
44
|
-
|
|
45
|
-
# ---- 2. 仪表盘 Harness 面板(dsh 当前为未安装态)----
|
|
46
|
-
await page.goto(BASE + '/', wait_until='domcontentloaded', timeout=90000)
|
|
47
|
-
ok('/workshop' not in page.url, f'仪表盘未跳登录门(url={page.url})')
|
|
48
|
-
await page.wait_for_selector('.harness-row .h-item', timeout=90000)
|
|
49
|
-
await page.wait_for_timeout(1500)
|
|
50
|
-
items = await page.locator('.harness-row .h-item').count()
|
|
51
|
-
ok(items == 6, f'仪表盘 6 引擎条目(实际 {items})')
|
|
52
|
-
off = page.locator('.harness-row .h-item.off')
|
|
53
|
-
off_n = await off.count()
|
|
54
|
-
if EXPECT_OFF:
|
|
55
|
-
ok(off_n == 1, f'未安装灰化条目=1(实际 {off_n})')
|
|
56
|
-
if off_n == 1:
|
|
57
|
-
txt = await off.first.inner_text()
|
|
58
|
-
ok('未安装' in txt and 'dsh' in txt, f'灰化条目为 dsh:{txt!r}')
|
|
59
|
-
total = await page.locator('.dp-hd:has-text("执行引擎") .fleet-total').inner_text()
|
|
60
|
-
ok('5/6' in total, f'就绪计数 5/6(实际 {total!r})')
|
|
61
|
-
else:
|
|
62
|
-
print(f' · 灰化条目={off_n}(当前环境探测结果;全部安装时为 0)', flush=True)
|
|
63
|
-
total = await page.locator('.dp-hd:has-text("执行引擎") .fleet-total').inner_text()
|
|
64
|
-
print(f' · 就绪计数={total!r}', flush=True)
|
|
65
|
-
await page.screenshot(path=f'{SHOT}/harness-1-dashboard-off.png', full_page=False)
|
|
66
|
-
|
|
67
|
-
# ---- 3. agents 模板页:选择器禁用未安装项 ----
|
|
68
|
-
await page.goto(BASE + '/workshop/agents', wait_until='domcontentloaded', timeout=90000)
|
|
69
|
-
await page.click('button:has-text("新建模板")')
|
|
70
|
-
await page.wait_for_selector('.ant-modal .ant-select', timeout=10000)
|
|
71
|
-
await page.click('.ant-modal .ant-select')
|
|
72
|
-
await page.wait_for_selector('.ant-select-dropdown .ant-select-item-option', timeout=10000)
|
|
73
|
-
opts = await page.locator('.ant-select-dropdown .ant-select-item-option').all_inner_texts()
|
|
74
|
-
disabled = page.locator('.ant-select-dropdown .ant-select-item-option-disabled')
|
|
75
|
-
dis_n = await disabled.count()
|
|
76
|
-
if EXPECT_OFF:
|
|
77
|
-
ok(any('未安装' in o for o in opts), f'下拉含(未安装)标记:{[o for o in opts if "未安装" in o]!r}')
|
|
78
|
-
ok(dis_n == 1, f'禁用选项=1(实际 {dis_n})')
|
|
79
|
-
if dis_n == 1:
|
|
80
|
-
ok('dsh' in (await disabled.first.inner_text()), '禁用项为 dsh')
|
|
81
|
-
else:
|
|
82
|
-
print(f' · 下拉选项={len(opts)},禁用={dis_n}(全部安装时为 0)', flush=True)
|
|
83
|
-
await page.screenshot(path=f'{SHOT}/harness-2-agent-select-disabled.png', full_page=False)
|
|
84
|
-
await page.keyboard.press('Escape')
|
|
85
|
-
|
|
86
|
-
# ---- 4. teams 页:成员选择禁用 dsh 模板(视觉记录)----
|
|
87
|
-
await page.goto(BASE + '/workshop/teams', wait_until='domcontentloaded', timeout=90000)
|
|
88
|
-
await page.wait_for_timeout(1500)
|
|
89
|
-
await page.screenshot(path=f'{SHOT}/harness-3-teams.png', full_page=False)
|
|
90
|
-
|
|
91
|
-
await browser.close()
|
|
92
|
-
|
|
93
|
-
fails = [r for r in results if not r[0]]
|
|
94
|
-
print(f'\nUI RESULT: {len(results) - len(fails)} pass / {len(fails)} fail')
|
|
95
|
-
sys.exit(1 if fails else 0)
|
|
96
|
-
|
|
97
|
-
asyncio.run(main())
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 端到端验证:每个 AgentRuntime 的任务队列管理 + 实时状态追踪 + lead 统一调度分发。
|
|
3
|
-
*
|
|
4
|
-
* 场景 A(用户要求的实际测试):
|
|
5
|
-
* 1. 模拟用户向 channel 连发两个任务需求(submitChannelTask ×2)
|
|
6
|
-
* 2. 观察 lead FIFO 处理外部队列:先提交的任务先分解(dispatch 子任务 createdAt 严格递增)
|
|
7
|
-
* 3. 一轮内两个任务分给两个不同 worker(池化分配,不重复占同一个空闲 worker)
|
|
8
|
-
* 4. worker 接任务 → busy(带 currentTaskId)→ 执行 → 完成后查队列取下一个 → 队列空 → idle
|
|
9
|
-
* 5. 执行中再补两个任务 → 排队等待 → 逐个消化 → 全部完成后 worker idle
|
|
10
|
-
*
|
|
11
|
-
* 场景 B(lead 对 worker 队列的增删改查与调配,走 omp host tool 同款 manager 通道):
|
|
12
|
-
* - 查:queueViewOf 看 worker 待执行/执行中/已完成
|
|
13
|
-
* - 改:updateTask 修改排队任务内容,队列投递自动刷新
|
|
14
|
-
* - 调配:reassignTask 把排队任务迁移到另一 worker,旧 assignee 队列自动清退
|
|
15
|
-
* - 删:cancelTask 取消排队任务,过期投递作废,worker 永不执行
|
|
16
|
-
*
|
|
17
|
-
* 场景 C(omp rpc 原生集成冒烟):
|
|
18
|
-
* - 真实 omp 子进程(--mode rpc)注册全部 host tools(含 5 个新增队列管理工具)
|
|
19
|
-
*
|
|
20
|
-
* 运行: npx tsx scripts/e2e-task-queue.ts
|
|
21
|
-
*/
|
|
22
|
-
import type { DatabaseSync } from 'node:sqlite'
|
|
23
|
-
import { openWorkshopDb } from '../server/services/workshop/db/database'
|
|
24
|
-
import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
|
|
25
|
-
import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
|
|
26
|
-
import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
|
|
27
|
-
import { createTaskRepo } from '../server/services/workshop/db/task.repo'
|
|
28
|
-
import { createMemoryRepo } from '../server/services/workshop/db/memory.repo'
|
|
29
|
-
import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
|
|
30
|
-
import { createTeamRepo } from '../server/services/workshop/db/team.repo'
|
|
31
|
-
import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
|
|
32
|
-
import { createMessageRepo } from '../server/services/workshop/db/message.repo'
|
|
33
|
-
import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
|
|
34
|
-
import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
35
|
-
import type { AgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
36
|
-
import { createAgentImpl } from '../server/services/workshop/agents/factory'
|
|
37
|
-
import { monitorChannel } from '../server/services/workshop/runtime/monitor'
|
|
38
|
-
import { HOST_TOOLS } from '../server/services/workshop/agents/omp-agent'
|
|
39
|
-
import { OmpRpcClient } from '../server/services/workshop/agents/adapters/omp-rpc-client'
|
|
40
|
-
import type { AgentTaskQueueView } from '../server/services/workshop/types/task'
|
|
41
|
-
|
|
42
|
-
let failures = 0
|
|
43
|
-
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
|
|
44
|
-
|
|
45
|
-
function check(name: string, ok: boolean, detail = ''): void {
|
|
46
|
-
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
47
|
-
if (!ok) failures += 1
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeManager(db: DatabaseSync): AgentChannelManager {
|
|
51
|
-
return createAgentChannelManager({
|
|
52
|
-
repos: {
|
|
53
|
-
channels: createChannelRepo(db),
|
|
54
|
-
agents: createAgentRepo(db),
|
|
55
|
-
channelAgents: createChannelAgentRepo(db),
|
|
56
|
-
messages: createMessageRepo(db),
|
|
57
|
-
subscriptions: createSubscriptionRepo(db),
|
|
58
|
-
tasks: createTaskRepo(db),
|
|
59
|
-
memories: createMemoryRepo(db),
|
|
60
|
-
|
|
61
|
-
channelEvents: createChannelEventRepo(db),
|
|
62
|
-
teams: createTeamRepo(db),
|
|
63
|
-
teamMembers: createTeamMemberRepo(db),
|
|
64
|
-
},
|
|
65
|
-
implFactory: createAgentImpl,
|
|
66
|
-
db,
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** manager 内部 TaskEngine 访问(与运行时同一实例;ws.ts 同风格类型收窄) */
|
|
71
|
-
function engineOf(manager: AgentChannelManager) {
|
|
72
|
-
return (manager as unknown as {
|
|
73
|
-
getTaskEngine(): {
|
|
74
|
-
get(id: string): { id: string, title: string, state: string, assigneeId: string, createdAt: string, parentId?: string } | undefined
|
|
75
|
-
list(channelId: string): Array<{ id: string, title: string, state: string, assigneeId: string, createdAt: string, parentId?: string }>
|
|
76
|
-
queueViewOf(channelId: string, agentId: string): AgentTaskQueueView
|
|
77
|
-
}
|
|
78
|
-
}).getTaskEngine()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function waitUntil(pred: () => boolean, timeoutMs: number): Promise<boolean> {
|
|
82
|
-
const deadline = Date.now() + timeoutMs
|
|
83
|
-
while (Date.now() < deadline) {
|
|
84
|
-
if (pred()) return true
|
|
85
|
-
await sleep(20)
|
|
86
|
-
}
|
|
87
|
-
return pred()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** 场景 A + A2:两任务 FIFO 分发 → 双 worker 执行 → 追加任务排队消化 → 全部完成转空闲 */
|
|
91
|
-
async function scenarioDispatchAndDrain(manager: AgentChannelManager): Promise<void> {
|
|
92
|
-
console.log('\n━━━ 场景 A:外部两任务 FIFO 分发 + worker 执行 + 追加排队消化 ━━━')
|
|
93
|
-
const ch = await manager.createChannel({
|
|
94
|
-
name: 'queue-e2e-A',
|
|
95
|
-
leadAgent: { name: 'lead-甲', harness: 'mock', config: { delayMs: 0 } },
|
|
96
|
-
})
|
|
97
|
-
const wTplA = await manager.createAgent({ name: 'worker-A', harness: 'mock', config: { delayMs: 200 } })
|
|
98
|
-
const wTplB = await manager.createAgent({ name: 'worker-B', harness: 'mock', config: { delayMs: 200 } })
|
|
99
|
-
await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTplA.id, role: 'worker' })
|
|
100
|
-
await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTplB.id, role: 'worker' })
|
|
101
|
-
|
|
102
|
-
const members = await manager.listChannelAgents(ch.channelId)
|
|
103
|
-
const workers = members.filter(m => m.role === 'worker')
|
|
104
|
-
const engine = engineOf(manager)
|
|
105
|
-
|
|
106
|
-
// 先激活 channel(装配 bus + 调度循环),monitor 才能订阅到实时事件流
|
|
107
|
-
manager.ensureChannelActive(ch.channelId, { tickMs: 50 })
|
|
108
|
-
const mon = monitorChannel(manager, ch.channelId)
|
|
109
|
-
const p1 = await manager.submitChannelTask({ channelId: ch.channelId, title: '任务一:统计苹果数', description: '读取 input 文件统计水果' })
|
|
110
|
-
const p2 = await manager.submitChannelTask({ channelId: ch.channelId, title: '任务二:汇总会议纪要', description: '汇总 note 文件' })
|
|
111
|
-
console.log(` 用户提交:P1=${p1.id.slice(0, 8)} P2=${p2.id.slice(0, 8)}`)
|
|
112
|
-
|
|
113
|
-
// 2. lead FIFO:两个父任务都被分解出子任务
|
|
114
|
-
const childOf = (parentId: string) => engine.list(ch.channelId).find(t => t.parentId === parentId)
|
|
115
|
-
const bothDispatched = await waitUntil(() => !!childOf(p1.id) && !!childOf(p2.id), 10_000)
|
|
116
|
-
check('lead FIFO 处理外部队列:两个任务都被分解分发', bothDispatched)
|
|
117
|
-
const c1 = childOf(p1.id)!
|
|
118
|
-
const c2 = childOf(p2.id)!
|
|
119
|
-
check('FIFO 顺序:先提交的任务子任务先创建', c1.createdAt <= c2.createdAt, `c1@${c1.createdAt.slice(11, 23)} c2@${c2.createdAt.slice(11, 23)}`)
|
|
120
|
-
|
|
121
|
-
// 3. 一轮内两个任务去了两个不同 worker(池化分配;若都给同一 worker 则说明分配退化)
|
|
122
|
-
check('一轮内两任务分给不同 worker(最优分配)', c1.assigneeId !== c2.assigneeId, `w=${c1.assigneeId.slice(0, 8)}/${c2.assigneeId.slice(0, 8)}`)
|
|
123
|
-
|
|
124
|
-
// 4. 执行中追加两个任务 → 排队,worker 完成当前任务后按 FIFO 消化
|
|
125
|
-
const p3 = await manager.submitChannelTask({ channelId: ch.channelId, title: '任务三:整理清单', description: '第三需求' })
|
|
126
|
-
const p4 = await manager.submitChannelTask({ channelId: ch.channelId, title: '任务四:输出报告', description: '第四需求' })
|
|
127
|
-
const allDone = await waitUntil(
|
|
128
|
-
() => [p1, p2, p3, p4].every(p => engine.get(p.id)?.state === 'COMPLETED'),
|
|
129
|
-
30_000,
|
|
130
|
-
)
|
|
131
|
-
check('4 个外部任务全部完成(排队任务被逐个消化)', allDone,
|
|
132
|
-
[p1, p2, p3, p4].map(p => `${p.id.slice(0, 8)}=${engine.get(p.id)?.state}`).join(' '))
|
|
133
|
-
|
|
134
|
-
// 5. 每个 worker 内部执行顺序 = 其名下子任务(按 assignee 归属)创建顺序 = FIFO。
|
|
135
|
-
// 过滤按 assignee 而非 transition by:父任务 WAITING→WORKING 的迁移 by=子任务 assignee,
|
|
136
|
-
// 但那是 lead 汇总相位,不是该 worker 的执行相位。
|
|
137
|
-
const workingTasks = mon.events
|
|
138
|
-
.filter((e): e is Extract<typeof e, { taskId: string }> => e.kind === 'task.status' && e.state === 'WORKING')
|
|
139
|
-
.map(e => engine.get(e.taskId))
|
|
140
|
-
.filter((t): t is NonNullable<typeof t> => t !== undefined)
|
|
141
|
-
for (const w of workers) {
|
|
142
|
-
const seq = workingTasks.filter(t => t.assigneeId === w.id)
|
|
143
|
-
const fifo = seq.every((t, i) => i === 0 || seq[i - 1]!.createdAt <= t.createdAt)
|
|
144
|
-
check(`worker ${w.name} 执行顺序 FIFO(${seq.length} 个)`, seq.length >= 2 && fifo,
|
|
145
|
-
seq.map(t => t.title.slice(0, 12)).join(' → '))
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 6. 状态实时追踪:busy 事件带 currentTaskId;全部完成后 worker 空闲且队列空
|
|
149
|
-
const busyWithContext = mon.events.some(e =>
|
|
150
|
-
e.kind === 'agent.status' && e.state === 'busy' && e.currentTaskId != null)
|
|
151
|
-
check('agent.status busy 事件携带 currentTaskId(实时状态追踪)', busyWithContext)
|
|
152
|
-
|
|
153
|
-
const workersIdle = await waitUntil(() =>
|
|
154
|
-
workers.every(w => manager.getChannelAgent(w.id)?.runtimeState === 'idle'), 10_000)
|
|
155
|
-
check('队列清空后全部 worker 标记 idle', workersIdle)
|
|
156
|
-
for (const w of workers) {
|
|
157
|
-
const q = engine.queueViewOf(ch.channelId, w.id)
|
|
158
|
-
check(`worker ${w.name} 队列空 + 已完成 ${q.completed.length} 个`, q.queued.length === 0 && !q.current && q.completed.length >= 2)
|
|
159
|
-
}
|
|
160
|
-
mon.stop()
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** 场景 B:lead 对 worker 队列的查/改/调配/删(omp host tool 同款 manager 通道) */
|
|
164
|
-
async function scenarioQueueCrud(manager: AgentChannelManager): Promise<void> {
|
|
165
|
-
console.log('\n━━━ 场景 B:lead 对 worker 队列的增删改查与调配 ━━━')
|
|
166
|
-
const ch = await manager.createChannel({
|
|
167
|
-
name: 'queue-e2e-B',
|
|
168
|
-
leadAgent: { name: 'lead-乙', harness: 'mock', config: { delayMs: 0 } },
|
|
169
|
-
})
|
|
170
|
-
const wTpl1 = await manager.createAgent({ name: 'worker-甲', harness: 'mock', config: { delayMs: 500 } })
|
|
171
|
-
const wTpl2 = await manager.createAgent({ name: 'worker-乙', harness: 'mock', config: { delayMs: 500 } })
|
|
172
|
-
await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTpl1.id, role: 'worker' })
|
|
173
|
-
await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTpl2.id, role: 'worker' })
|
|
174
|
-
|
|
175
|
-
const members = await manager.listChannelAgents(ch.channelId)
|
|
176
|
-
const lead = members.find(m => m.role === 'lead')!
|
|
177
|
-
const [w1, w2] = members.filter(m => m.role === 'worker')
|
|
178
|
-
const engine = engineOf(manager)
|
|
179
|
-
|
|
180
|
-
// 激活 channel 装配 bus(不经 submitChannelTask,避免占位任务污染 worker 队列)
|
|
181
|
-
manager.ensureChannelActive(ch.channelId, { tickMs: 50 })
|
|
182
|
-
const mon = monitorChannel(manager, ch.channelId)
|
|
183
|
-
|
|
184
|
-
// lead 直接分发(= omp host tool dispatch_task 同款通道):
|
|
185
|
-
// C_a → w1(立即执行),C_b → w1(排队),C_c → w2(立即执行)
|
|
186
|
-
const ca = await manager.dispatchTask(ch.channelId, lead.id, { assigneeId: w1.id, title: 'C_a-首批' })
|
|
187
|
-
const cb = await manager.dispatchTask(ch.channelId, lead.id, { assigneeId: w1.id, title: 'C_b-次批' })
|
|
188
|
-
const cc = await manager.dispatchTask(ch.channelId, lead.id, { assigneeId: w2.id, title: 'C_c-并行' })
|
|
189
|
-
|
|
190
|
-
// 查:w1 执行中 C_a、队列 [C_b];w2 执行中 C_c
|
|
191
|
-
await waitUntil(() => engine.get(ca.id)?.state === 'WORKING' && engine.get(cc.id)?.state === 'WORKING', 10_000)
|
|
192
|
-
const q1 = engine.queueViewOf(ch.channelId, w1.id)
|
|
193
|
-
check('查:w1 执行中 C_a + 队列 [C_b]', q1.current?.id === ca.id && q1.queued.map(t => t.id).join() === cb.id)
|
|
194
|
-
|
|
195
|
-
// 改:更新排队任务 C_b 内容
|
|
196
|
-
const updated = await manager.updateTask(ch.channelId, lead.id, cb.id, { title: 'C_b-改后内容', description: '改描述' })
|
|
197
|
-
check('改:排队任务标题已更新', updated.title === 'C_b-改后内容' && engine.queueViewOf(ch.channelId, w1.id).queued[0]?.title === 'C_b-改后内容')
|
|
198
|
-
|
|
199
|
-
// 调配:C_b 从 w1 迁到 w2;w1 队列清退,w2 队列接收
|
|
200
|
-
const reassigned = await manager.reassignTask(ch.channelId, lead.id, cb.id, w2.id)
|
|
201
|
-
const q1b = engine.queueViewOf(ch.channelId, w1.id)
|
|
202
|
-
const q2b = engine.queueViewOf(ch.channelId, w2.id)
|
|
203
|
-
check('调配:assignee 已换到 w2', reassigned.assigneeId === w2.id && engine.get(cb.id)?.assigneeId === w2.id)
|
|
204
|
-
check('调配:w1 队列已清退', q1b.queued.length === 0 && !q1b.queued.some(t => t.id === cb.id))
|
|
205
|
-
check('调配:w2 队列接收(FIFO 排在当前任务后)', q2b.queued.some(t => t.id === cb.id))
|
|
206
|
-
|
|
207
|
-
// 删:向 w2 追加 C_d 后立即取消;过期投递作废
|
|
208
|
-
const cd = await manager.dispatchTask(ch.channelId, lead.id, { assigneeId: w2.id, title: 'C_d-待删' })
|
|
209
|
-
const canceled = await manager.cancelTask(ch.channelId, lead.id, { taskId: cd.id })
|
|
210
|
-
check('删:任务已 CANCELED', canceled.state === 'CANCELED')
|
|
211
|
-
|
|
212
|
-
// 终态:C_a/C_c/C_b 完成(调配后 C_b 由 w2 执行),C_d 永不执行
|
|
213
|
-
const settled = await waitUntil(
|
|
214
|
-
() => ['COMPLETED', 'FAILED', 'CANCELED'].includes(engine.get(ca.id)?.state ?? '')
|
|
215
|
-
&& ['COMPLETED', 'FAILED', 'CANCELED'].includes(engine.get(cb.id)?.state ?? '')
|
|
216
|
-
&& ['COMPLETED', 'FAILED', 'CANCELED'].includes(engine.get(cc.id)?.state ?? ''),
|
|
217
|
-
30_000,
|
|
218
|
-
)
|
|
219
|
-
check('C_a/C_b/C_c 全部完成', settled,
|
|
220
|
-
`C_a=${engine.get(ca.id)?.state} C_b=${engine.get(cb.id)?.state} C_c=${engine.get(cc.id)?.state}`)
|
|
221
|
-
check('调配后的 C_b 由 w2 执行完成', engine.get(cb.id)?.state === 'COMPLETED' && engine.get(cb.id)?.assigneeId === w2.id)
|
|
222
|
-
check('取消的 C_d 未被执行(仍 CANCELED)', engine.get(cd.id)?.state === 'CANCELED')
|
|
223
|
-
|
|
224
|
-
// w1 从未执行 C_b(调配后无 WORKING 事件);w2 从未执行 C_d
|
|
225
|
-
const worked = mon.events.filter(e => e.kind === 'task.status' && e.state === 'WORKING')
|
|
226
|
-
check('w1 从未执行已调配走的 C_b', !worked.some(e => (e as { taskId: string }).taskId === cb.id && (e as { agentId?: string }).agentId === w1.id))
|
|
227
|
-
check('w2 从未执行已取消的 C_d', !worked.some(e => (e as { taskId: string }).taskId === cd.id))
|
|
228
|
-
|
|
229
|
-
// 过期投递作废:w2 邮箱无 C_d 的 pending 消息
|
|
230
|
-
const msgs = (manager as unknown as {
|
|
231
|
-
deps: { repos: { messages: { listPendingByChannelAgent(chId: string, aId: string): Array<{ taskId: string | null }> } } }
|
|
232
|
-
}).deps.repos.messages.listPendingByChannelAgent(ch.channelId, w2.id)
|
|
233
|
-
check('取消任务的过期投递已作废(无 pending)', !msgs.some(m => m.taskId === cd.id))
|
|
234
|
-
|
|
235
|
-
// lead 队列总览(host tool get_queue_overview 同款通道)
|
|
236
|
-
const overview = await manager.queueOverview(ch.channelId, lead.id)
|
|
237
|
-
check('queueOverview 返回全员实时状态', overview.length === 3 && overview.every(s => s.state === 'idle' || s.state === 'busy' || s.state === 'stopped'))
|
|
238
|
-
mon.stop()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** 场景 C:真实 omp rpc 子进程注册全部 host tools(原生集成冒烟,无 LLM 调用) */
|
|
242
|
-
async function scenarioOmpRpc(): Promise<void> {
|
|
243
|
-
console.log('\n━━━ 场景 C:omp rpc 原生集成(host tools 注册冒烟)━━━')
|
|
244
|
-
const client = new OmpRpcClient({ command: 'omp' })
|
|
245
|
-
try {
|
|
246
|
-
await client.start()
|
|
247
|
-
const resp = await client.send({ type: 'set_host_tools', tools: HOST_TOOLS })
|
|
248
|
-
check(`omp rpc 就绪 + 注册 ${HOST_TOOLS.length} 个 host tools(含 5 个队列管理工具)`, resp.success === true)
|
|
249
|
-
const names = HOST_TOOLS.map(t => t.name)
|
|
250
|
-
check('新增队列工具均在注册列表', ['get_my_task_queue', 'get_queue_overview', 'reassign_task', 'update_task', 'cancel_task'].every(n => names.includes(n)))
|
|
251
|
-
}
|
|
252
|
-
finally {
|
|
253
|
-
await client.dispose()
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function main(): Promise<void> {
|
|
258
|
-
console.log('━━━ AgentRuntime 任务队列管理 + 状态追踪 + lead 调度 端到端 ━━━')
|
|
259
|
-
const db = openWorkshopDb(':memory:')
|
|
260
|
-
const manager = makeManager(db)
|
|
261
|
-
try {
|
|
262
|
-
await scenarioDispatchAndDrain(manager)
|
|
263
|
-
await scenarioQueueCrud(manager)
|
|
264
|
-
await scenarioOmpRpc()
|
|
265
|
-
}
|
|
266
|
-
finally {
|
|
267
|
-
await manager.shutdown()
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
|
|
271
|
-
process.exit(failures === 0 ? 0 : 1)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
main().catch((e) => {
|
|
275
|
-
console.error('测试异常:', e)
|
|
276
|
-
process.exit(1)
|
|
277
|
-
})
|