agentworkshop 0.7.40 → 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.
Files changed (327) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B8uy9iRW.js +1 -0
  3. package/.output/public/_nuxt/BGTYYQH-.js +1 -0
  4. package/.output/public/_nuxt/BUmHEJ77.js +2 -0
  5. package/.output/public/_nuxt/BY7YrV_D.js +1 -0
  6. package/.output/public/_nuxt/Bqcgz95s.js +18 -0
  7. package/.output/public/_nuxt/C7UlqPeD.js +1 -0
  8. package/.output/public/_nuxt/CTmDn-YP.js +1 -0
  9. package/.output/public/_nuxt/Cs8pVeVG.js +1 -0
  10. package/.output/public/_nuxt/{DcsCcxp4.js → D98NH8kG.js} +1 -1
  11. package/.output/public/_nuxt/DKySmNPI.js +14 -0
  12. package/.output/public/_nuxt/Di0NY_ht.js +1 -0
  13. package/.output/public/_nuxt/Du9v6qGB.js +1 -0
  14. package/.output/public/_nuxt/{C_iYD-EB.js → EHlEMRii.js} +1 -1
  15. package/.output/public/_nuxt/Eke1fs-U.js +1 -0
  16. package/.output/public/_nuxt/{DeJykBFf.js → H0BcqZtT.js} +1 -1
  17. package/.output/public/_nuxt/{C4oW1HsE.js → HSYEkkce.js} +1 -1
  18. package/.output/public/_nuxt/{DZshyNLa.js → IREZ1eC2.js} +1 -1
  19. package/.output/public/_nuxt/{D_OTvQza.js → J9oU00D1.js} +1 -1
  20. package/.output/public/_nuxt/OmpTerminalPanel.BxR5-lvi.css +1 -0
  21. package/.output/public/_nuxt/SSyAPaHc.js +1 -0
  22. package/.output/public/_nuxt/TownView.li4EMQxK.css +1 -0
  23. package/.output/public/_nuxt/VdLk4jLE2.js +1 -0
  24. package/.output/public/_nuxt/_id_.BhsXoEuk.css +1 -0
  25. package/.output/public/_nuxt/_id_.DQyj-WKN.css +1 -0
  26. package/.output/public/_nuxt/{_wsId_.B4WiqkIS.css → _wsId_.CTx-BlfF.css} +1 -1
  27. package/.output/public/_nuxt/aShsv_rG.js +19 -0
  28. package/.output/public/_nuxt/{yF0aYMF1.js → aesgXKSr.js} +3 -3
  29. package/.output/public/_nuxt/agents.B21nUEUX.css +1 -0
  30. package/.output/public/_nuxt/aml.ekTjxFkf.css +1 -0
  31. package/.output/public/_nuxt/builds/latest.json +1 -1
  32. package/.output/public/_nuxt/builds/meta/b5506ef0-f48e-4f14-a6c0-d65e505d0b09.json +1 -0
  33. package/.output/public/_nuxt/ca4TJ3Op.js +1 -0
  34. package/.output/public/_nuxt/channel-templates.DplM1pJ3.css +1 -0
  35. package/.output/public/_nuxt/daq.BdVmoIfe.css +1 -0
  36. package/.output/public/_nuxt/dcw.DCgyzpWV.css +1 -0
  37. package/.output/public/_nuxt/{default.J32SgqFY.css → default.Bh3L2HU8.css} +1 -1
  38. package/.output/public/_nuxt/lHAxdSsj.js +1 -0
  39. package/.output/public/_nuxt/pages.j9rzRocr.css +1 -0
  40. package/.output/public/_nuxt/plugins.DhiZbHU8.css +1 -0
  41. package/.output/public/_nuxt/settings.BRx9rIDg.css +1 -0
  42. package/.output/public/_nuxt/teams.BLlOPB_Z.css +1 -0
  43. package/.output/public/_nuxt/tokens.De8xzszn.css +1 -0
  44. package/.output/public/_nuxt/workshop.D1jFNkrn.css +1 -0
  45. package/.output/server/chunks/_/nitro.mjs +1058 -830
  46. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  47. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs → OmpTerminalPanel-CVvxpsF7.mjs} +15 -15
  48. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs.map → OmpTerminalPanel-CVvxpsF7.mjs.map} +1 -1
  49. package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs → TownView-CJ6IsDCc.mjs} +229 -228
  50. package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs.map → TownView-CJ6IsDCc.mjs.map} +1 -1
  51. package/.output/server/chunks/build/{_id_-BU1wfOID.mjs → _id_-CVD_H0ir.mjs} +149 -147
  52. package/.output/server/chunks/build/_id_-CVD_H0ir.mjs.map +1 -0
  53. package/.output/server/chunks/build/{_id_-BYsEua57.mjs → _id_-CtxMb3qp.mjs} +47 -46
  54. package/.output/server/chunks/build/_id_-CtxMb3qp.mjs.map +1 -0
  55. package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs → _wsId_-DpY3T-md.mjs} +516 -446
  56. package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs.map → _wsId_-DpY3T-md.mjs.map} +1 -1
  57. package/.output/server/chunks/build/{agents-CLQqJF2y.mjs → agents-DsCkuXDV.mjs} +49 -46
  58. package/.output/server/chunks/build/agents-DsCkuXDV.mjs.map +1 -0
  59. package/.output/server/chunks/build/{aml-_7FdFL2p.mjs → aml-CT0xKGKI.mjs} +122 -105
  60. package/.output/server/chunks/build/aml-CT0xKGKI.mjs.map +1 -0
  61. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs +117 -0
  62. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs.map +1 -0
  63. package/.output/server/chunks/build/{channel-templates-p6SZNjlM.mjs → channel-templates-Dla9FbYa.mjs} +49 -37
  64. package/.output/server/chunks/build/channel-templates-Dla9FbYa.mjs.map +1 -0
  65. package/.output/server/chunks/build/{daq-cgyjKmJu.mjs → daq-DWWSKGAV.mjs} +95 -93
  66. package/.output/server/chunks/build/daq-DWWSKGAV.mjs.map +1 -0
  67. package/.output/server/chunks/build/{dcw-CoTxBlAZ.mjs → dcw-CT8k17_E.mjs} +46 -45
  68. package/.output/server/chunks/build/dcw-CT8k17_E.mjs.map +1 -0
  69. package/.output/server/chunks/build/{default-BE4r0FZN.mjs → default-D3525UFX.mjs} +16 -15
  70. package/.output/server/chunks/build/{default-BE4r0FZN.mjs.map → default-D3525UFX.mjs.map} +1 -1
  71. package/.output/server/chunks/build/{logs-BSOeZwwI.mjs → logs--dByNyhw.mjs} +2 -2
  72. package/.output/server/chunks/build/{logs-BSOeZwwI.mjs.map → logs--dByNyhw.mjs.map} +1 -1
  73. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs → monitor-Dh2Kvaq5.mjs} +2 -2
  74. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs.map → monitor-Dh2Kvaq5.mjs.map} +1 -1
  75. package/.output/server/chunks/build/{pages-DFxjn9Xa.mjs → pages-BHcSBM2D.mjs} +26 -20
  76. package/.output/server/chunks/build/pages-BHcSBM2D.mjs.map +1 -0
  77. package/.output/server/chunks/build/{plugins-lGlUr1lj.mjs → plugins-BNgIi61W.mjs} +23 -17
  78. package/.output/server/chunks/build/plugins-BNgIi61W.mjs.map +1 -0
  79. package/.output/server/chunks/build/{settings-Ddd1qx2Z.mjs → settings-ipYBDWdn.mjs} +45 -44
  80. package/.output/server/chunks/build/settings-ipYBDWdn.mjs.map +1 -0
  81. package/.output/server/chunks/build/{teams-MEDLZdMn.mjs → teams-5lx-CwFa.mjs} +47 -44
  82. package/.output/server/chunks/build/teams-5lx-CwFa.mjs.map +1 -0
  83. package/.output/server/chunks/build/{tokens-C7fTM5C2.mjs → tokens-q-IDT8Io.mjs} +73 -53
  84. package/.output/server/chunks/build/tokens-q-IDT8Io.mjs.map +1 -0
  85. package/.output/server/chunks/build/{town-qMPql9Se.mjs → town-QJWB5wng.mjs} +5 -4
  86. package/.output/server/chunks/build/town-QJWB5wng.mjs.map +1 -0
  87. package/.output/server/chunks/build/{useDaqStream-CIBaiGAl.mjs → useDaqStream-DfCFpb3p.mjs} +9 -3
  88. package/.output/server/chunks/build/useDaqStream-DfCFpb3p.mjs.map +1 -0
  89. package/.output/server/chunks/build/{useDcwStream-CBbiYnWe.mjs → useDcwStream-UPiX-3iO.mjs} +11 -2
  90. package/.output/server/chunks/build/useDcwStream-UPiX-3iO.mjs.map +1 -0
  91. package/.output/server/chunks/build/{workshop-BL5bhfs0.mjs → workshop-BIlCj_ZP.mjs} +45 -45
  92. package/.output/server/chunks/build/workshop-BIlCj_ZP.mjs.map +1 -0
  93. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  94. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +3 -3
  95. package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +1 -1
  96. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
  97. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
  98. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +1 -1
  99. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +1 -1
  100. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
  101. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
  102. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  103. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
  104. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
  105. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
  106. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  107. package/.output/server/chunks/routes/api/workshop/index5.get.mjs +3 -1
  108. package/.output/server/chunks/routes/api/workshop/index5.get.mjs.map +1 -1
  109. package/.output/server/chunks/routes/api/workshop/index6.get.mjs +3 -1
  110. package/.output/server/chunks/routes/api/workshop/index6.get.mjs.map +1 -1
  111. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  112. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  113. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  114. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  115. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  116. package/.output/server/chunks/routes/renderer.mjs +2 -2
  117. package/.output/server/chunks/virtual/entry.mjs +639 -88
  118. package/.output/server/chunks/virtual/entry.mjs.map +1 -1
  119. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  120. package/.output/server/package.json +1 -1
  121. package/README-zh.md +16 -6
  122. package/README.md +17 -7
  123. package/app/components/AppHeader.vue +961 -959
  124. package/app/components/workshop/A2aRpcDebugger.vue +237 -237
  125. package/app/components/workshop/AgentInspectorDrawer.vue +424 -424
  126. package/app/components/workshop/AgentLanesView.vue +998 -998
  127. package/app/components/workshop/ArtifactCard.vue +120 -120
  128. package/app/components/workshop/AssetLibrary.vue +452 -452
  129. package/app/components/workshop/ChannelSessionList.vue +812 -804
  130. package/app/components/workshop/CommandPalette.vue +269 -269
  131. package/app/components/workshop/Composer.vue +827 -827
  132. package/app/components/workshop/DeviceTwinPanel.vue +643 -643
  133. package/app/components/workshop/EventBlock.vue +602 -602
  134. package/app/components/workshop/FileSelectorModal.vue +188 -188
  135. package/app/components/workshop/InspectorPanel.vue +301 -301
  136. package/app/components/workshop/MemoryPanel.vue +364 -364
  137. package/app/components/workshop/MultiChannelView.vue +189 -189
  138. package/app/components/workshop/TaskBoardView.vue +686 -686
  139. package/app/components/workshop/TaskInspectorDrawer.vue +390 -390
  140. package/app/components/workshop/TranscriptTimeline.vue +601 -601
  141. package/app/components/workshop/blocks/ClusterArtifact.vue +61 -61
  142. package/app/components/workshop/blocks/ClusterError.vue +85 -85
  143. package/app/components/workshop/blocks/ClusterLife.vue +87 -87
  144. package/app/components/workshop/blocks/ClusterMember.vue +95 -93
  145. package/app/components/workshop/blocks/ClusterMemory.vue +84 -84
  146. package/app/components/workshop/blocks/ClusterOther.vue +67 -67
  147. package/app/components/workshop/blocks/ClusterStatus.vue +149 -147
  148. package/app/components/workshop/blocks/ClusterTool.vue +158 -158
  149. package/app/components/workshop/terminal/OmpTerminalPanel.vue +1011 -1011
  150. package/app/components/workshop/town/TownView.vue +56 -56
  151. package/app/composables/workshop/useDaqStream.ts +7 -4
  152. package/app/composables/workshop/useDcwStream.ts +8 -2
  153. package/app/middleware/locale-cookie.global.ts +22 -0
  154. package/app/pages/aml/index.vue +24 -2
  155. package/app/pages/daq/[id].vue +20 -16
  156. package/app/pages/daq/index.vue +2310 -2307
  157. package/app/pages/dcw/[id].vue +26 -23
  158. package/app/pages/dcw/index.vue +3 -3
  159. package/app/pages/index.vue +987 -981
  160. package/app/pages/plugins/index.vue +317 -311
  161. package/app/pages/settings.vue +1564 -1562
  162. package/app/pages/tokens.vue +15 -15
  163. package/app/pages/workshop/agents.vue +612 -609
  164. package/app/pages/workshop/channel-templates.vue +18 -11
  165. package/app/pages/workshop/index.vue +668 -668
  166. package/app/pages/workshop/teams.vue +743 -740
  167. package/app/pages/workshop/w/[wsId].vue +704 -704
  168. package/app/plugins/locale-restore.client.ts +16 -1
  169. package/app/utils/builtin-catalog.ts +104 -0
  170. package/config.yml +170 -170
  171. package/docs/cli.en.md +2 -2
  172. package/docs/cli.md +2 -2
  173. package/docs/full-test-plan.md +2 -3
  174. package/docs/plugins.en.md +33 -1
  175. package/docs/plugins.md +29 -1
  176. package/docs/sdk.en.md +1 -1
  177. package/docs/sdk.md +1 -1
  178. package/i18n/locales/en.ts +293 -35
  179. package/i18n/locales/zh-CN.ts +265 -7
  180. package/package.json +4 -3
  181. package/scripts/_dbg-perms-e2e.mjs +132 -109
  182. package/scripts/_demo-agentteam-capture.mjs +278 -0
  183. package/scripts/_e2e-ui-functional.mjs +371 -0
  184. package/scripts/_i18n-en-cjk-scan.mjs +28 -0
  185. package/scripts/_i18n-en-verify.mjs +107 -0
  186. package/scripts/_i18n-extra-checks.mjs +53 -0
  187. package/scripts/_i18n-scan-hardcoded.mjs +180 -0
  188. package/scripts/_i18n-verify-keys.mjs +50 -0
  189. package/scripts/_loop-biax-campaign.mjs +130 -0
  190. package/scripts/_sweep-button-contrast.mjs +87 -0
  191. package/scripts/test-serial-bridge.mjs +418 -0
  192. package/scripts/ui/e2e-interactive.mjs +328 -0
  193. package/scripts/ui/record-demo.mjs +557 -0
  194. package/scripts/ui/seed-industrial-demo.mjs +171 -0
  195. package/server/api/workshop/daq/index.get.ts +3 -1
  196. package/server/api/workshop/dcw/index.get.ts +3 -0
  197. package/server/plugins/aw-plugins.ts +9 -7
  198. package/server/plugins-builtin/diag-bridge/index.mjs +608 -661
  199. package/server/plugins-builtin/rag-bridge/index.mjs +57 -133
  200. package/server/plugins-builtin/serial-bridge/client.mjs +210 -0
  201. package/server/plugins-builtin/serial-bridge/drivers.mjs +409 -0
  202. package/server/plugins-builtin/serial-bridge/i18n.json +62 -0
  203. package/server/plugins-builtin/serial-bridge/index.mjs +247 -0
  204. package/server/plugins-builtin/serial-bridge/lib.mjs +208 -0
  205. package/server/plugins-builtin/serial-bridge/rtu.mjs +168 -0
  206. package/server/services/workshop/daq/daq-controller.ts +7 -2
  207. package/server/services/workshop/daq/drivers.ts +89 -3
  208. package/server/services/workshop/dcw/dcw-controller.ts +5 -1
  209. package/server/services/workshop/dcw/drivers.ts +85 -2
  210. package/server/services/workshop/dcw/plugin-bridge.ts +45 -0
  211. package/server/services/workshop/plugins/host.mjs +30 -0
  212. package/shared/daq-protocol.ts +15 -0
  213. package/.output/public/_nuxt/88GOowgO2.js +0 -1
  214. package/.output/public/_nuxt/BOt2SP4z.js +0 -1
  215. package/.output/public/_nuxt/BP-NyKVG.js +0 -18
  216. package/.output/public/_nuxt/BXxH0UQE.js +0 -1
  217. package/.output/public/_nuxt/BsROmY89.js +0 -1
  218. package/.output/public/_nuxt/ClILjvWH.js +0 -1
  219. package/.output/public/_nuxt/CzOCSGLe.js +0 -1
  220. package/.output/public/_nuxt/D8qvkfyY.js +0 -1
  221. package/.output/public/_nuxt/DCn7QZ5N.js +0 -2
  222. package/.output/public/_nuxt/DS8qj8EL.js +0 -1
  223. package/.output/public/_nuxt/DUjW5Yq5.js +0 -1
  224. package/.output/public/_nuxt/Dihnimlk.js +0 -19
  225. package/.output/public/_nuxt/Dqw1lpr-.js +0 -1
  226. package/.output/public/_nuxt/Dr9v6C0f.js +0 -14
  227. package/.output/public/_nuxt/Du9ZJtnj.js +0 -1
  228. package/.output/public/_nuxt/OmpTerminalPanel.B_Pl5Ynm.css +0 -1
  229. package/.output/public/_nuxt/TownView.f-YOdXRs.css +0 -1
  230. package/.output/public/_nuxt/_id_.CM4bIgKY.css +0 -1
  231. package/.output/public/_nuxt/_id_.bNV69Dtf.css +0 -1
  232. package/.output/public/_nuxt/agents.ZD0rVdHE.css +0 -1
  233. package/.output/public/_nuxt/aml.BN8ZEW4h.css +0 -1
  234. package/.output/public/_nuxt/builds/meta/f4707884-7cf4-4a31-80d2-e3d72b73745f.json +0 -1
  235. package/.output/public/_nuxt/channel-templates.BOtahzPi.css +0 -1
  236. package/.output/public/_nuxt/daq.B0D3dPAc.css +0 -1
  237. package/.output/public/_nuxt/dcw.8Y7FqCVN.css +0 -1
  238. package/.output/public/_nuxt/pages.D7aSDlUI.css +0 -1
  239. package/.output/public/_nuxt/plugins.sb4nDGqr.css +0 -1
  240. package/.output/public/_nuxt/qr6R6Ha1.js +0 -1
  241. package/.output/public/_nuxt/settings.BTgR-NiE.css +0 -1
  242. package/.output/public/_nuxt/teams.CxnxKVVs.css +0 -1
  243. package/.output/public/_nuxt/tokens.BFY7qv4f.css +0 -1
  244. package/.output/public/_nuxt/workshop.CR_QzeAN.css +0 -1
  245. package/.output/server/chunks/build/_id_-BU1wfOID.mjs.map +0 -1
  246. package/.output/server/chunks/build/_id_-BYsEua57.mjs.map +0 -1
  247. package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +0 -1
  248. package/.output/server/chunks/build/aml-_7FdFL2p.mjs.map +0 -1
  249. package/.output/server/chunks/build/channel-templates-p6SZNjlM.mjs.map +0 -1
  250. package/.output/server/chunks/build/daq-cgyjKmJu.mjs.map +0 -1
  251. package/.output/server/chunks/build/dcw-CoTxBlAZ.mjs.map +0 -1
  252. package/.output/server/chunks/build/pages-DFxjn9Xa.mjs.map +0 -1
  253. package/.output/server/chunks/build/plugins-lGlUr1lj.mjs.map +0 -1
  254. package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +0 -1
  255. package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +0 -1
  256. package/.output/server/chunks/build/tokens-C7fTM5C2.mjs.map +0 -1
  257. package/.output/server/chunks/build/town-qMPql9Se.mjs.map +0 -1
  258. package/.output/server/chunks/build/useDaqStream-CIBaiGAl.mjs.map +0 -1
  259. package/.output/server/chunks/build/useDcwStream-CBbiYnWe.mjs.map +0 -1
  260. package/.output/server/chunks/build/workshop-BL5bhfs0.mjs.map +0 -1
  261. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +0 -1
  262. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +0 -1
  263. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +0 -153
  264. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +0 -153
  265. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +0 -431
  266. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +0 -180
  267. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +0 -112
  268. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +0 -227
  269. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +0 -131
  270. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +0 -160
  271. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +0 -157
  272. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +0 -348
  273. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +0 -296
  274. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +0 -285
  275. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +0 -217
  276. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +0 -301
  277. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +0 -151
  278. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +0 -135
  279. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +0 -156
  280. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +0 -72
  281. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +0 -469
  282. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +0 -259
  283. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +0 -97
  284. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +0 -277
  285. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +0 -171
  286. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +0 -425
  287. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +0 -99
  288. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +0 -325
  289. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +0 -332
  290. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +0 -43
  291. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +0 -154
  292. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +0 -531
  293. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +0 -439
  294. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +0 -323
  295. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +0 -433
  296. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +0 -280
  297. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +0 -117
  298. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +0 -160
  299. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +0 -101
  300. package/scripts/.mimosa/hook-status/sess_aeaecb58-8833-4b83-b93c-050467d21e03-d944d71bb3.json +0 -14
  301. package/scripts/_dbg-audit-all-pages.mjs +0 -43
  302. package/scripts/_dbg-daq-frames-e2e.mjs +0 -118
  303. package/scripts/_dbg-daq-rowstyle-verify.mjs +0 -112
  304. package/scripts/_dbg-hitl-timeout-e2e.mjs +0 -93
  305. package/scripts/_dbg-live-line-e2e.mjs +0 -279
  306. package/scripts/_dbg-minimal-omp.mjs +0 -77
  307. package/scripts/_dbg-mode-nav.mjs +0 -173
  308. package/scripts/_dbg-real-scenario-timeline.log +0 -55
  309. package/scripts/_dbg-team-repro.mjs +0 -76
  310. package/scripts/_dbg-town-agents-probe.mjs +0 -60
  311. package/scripts/_dbg-visual-acceptance.mjs +0 -63
  312. package/scripts/_townview_style.css +0 -1185
  313. package/scripts/_townview_template.html +0 -1215
  314. package/server/data/agent-node-bindings.json +0 -554
  315. package/server/data/daq-templates.json +0 -16
  316. package/server/data/daqs.json +0 -7464
  317. package/server/data/dcw-lines.json +0 -1787
  318. package/server/data/dcw-products.json +0 -1626
  319. package/server/data/dcw-recipes.json +0 -4672
  320. package/server/data/dcw-rollback.json +0 -20754
  321. package/server/data/dcw-runs.json +0 -6354
  322. package/server/data/dcw-templates.json +0 -146
  323. package/server/data/dcw-writes.json +0 -15650
  324. package/server/data/dcws.json +0 -9033
  325. package/server/data/device-twins.json +0 -133
  326. package/server/data/line-runs.json +0 -322
  327. package/server/data/scene-layouts.json +0 -277
@@ -1,109 +1,132 @@
1
- // ============================================================
2
- // 产线权限 E2E:_dbg-perms-e2e.mjs <base> <adminPass>
3
- // 链路:admin 总览 → 建 plain 用户 → 默认全无权 → admin 授
4
- // 产线1=readonly / 产线2=operate → 可见性/写控/绑定逐项断言。
5
- // ============================================================
6
- const BASE = process.argv[2] ?? 'http://127.0.0.1:3021'
7
- const ADMIN_PASS = process.argv[3] ?? 'admin123'
8
- let step = 0
9
- const ok = (name, cond, detail = '') => {
10
- step++
11
- console.log(`${cond ? '✔' : '✖'} ${name}${detail ? ` — ${detail}` : ''}`)
12
- if (!cond) process.exitCode = 1
13
- }
14
- const api = async (path, opts = {}, tok) => {
15
- const res = await fetch(`${BASE}${path}`, {
16
- ...opts,
17
- headers: { 'content-type': 'application/json', ...(tok ? { authorization: `Bearer ${tok}` } : {}), ...opts.headers },
18
- })
19
- return { status: res.status, body: await res.json().catch(() => null) }
20
- }
21
- const login = async (email, password) => (await api('/api/users/login', { method: 'POST', body: JSON.stringify({ email, password }) })).body?.data?.token
22
-
23
- // ---- admin 登录 + 总览 ----
24
- const adminTok = await login('admin@awshop.local', ADMIN_PASS)
25
- ok('admin 登录', Boolean(adminTok))
26
- const ov = await api('/api/workshop/permissions', {}, adminTok)
27
- ok('admin 权限总览(lines+users)', ov.status === 200 && ov.body?.data?.lines?.length > 0 && ov.body?.data?.users?.length > 0, `lines=${ov.body?.data?.lines?.length} users=${ov.body?.data?.users?.length}`)
28
- const LINE1 = ov.body.data.lines[0]?.id
29
- const LINE2 = ov.body.data.lines[1]?.id
30
- ok('找到两条目标产线', Boolean(LINE1 && LINE2), `${LINE1} / ${LINE2}`)
31
-
32
- // ---- plain 用户(注册即 user 角色) ----
33
- const stamp = Date.now().toString(36)
34
- const plain = { name: `p-${stamp}`, email: `p-${stamp}@awshop.local`, password: 'Plain2026' }
35
- const reg = await api('/api/users/register', { method: 'POST', body: JSON.stringify(plain) })
36
- const plainTok = reg.body?.data?.token
37
- ok('plain 用户注册(user 角色)', reg.body?.data?.user?.role === 'user')
38
-
39
- // ---- 授权前:全无权 ----
40
- const d0 = await api('/api/workshop/dcw', {}, plainTok)
41
- const q0 = await api('/api/workshop/daq', {}, plainTok)
42
- ok('默认 dcw 产线不可见', d0.body?.data?.lines?.length === 0, `lines=${d0.body?.data?.lines?.length}`)
43
- ok('默认 dcw 节点不可见', d0.body?.data?.nodes?.length === 0, `nodes=${d0.body?.data?.nodes?.length}`)
44
- ok('默认 daq 节点不可见', q0.body?.data?.nodes?.length === 0, `nodes=${q0.body?.data?.nodes?.length}`)
45
-
46
- // ---- admin 授权:产线1=readonly, 产线2=operate ----
47
- const ov2 = await api('/api/workshop/permissions', {}, adminTok)
48
- const plainId = ov2.body.data.users.find(u => u.email === plain.email)?.id
49
- ok('总览含 plain 用户及其空 channels', Boolean(plainId))
50
- const put = await api('/api/workshop/permissions', { method: 'PUT', body: JSON.stringify({ userId: plainId, grants: [{ lineId: LINE1, mode: 'readonly' }, { lineId: LINE2, mode: 'operate' }] }) }, adminTok)
51
- ok('admin 批量授权(readonly+operate)', put.body?.data?.grants?.length === 2, JSON.stringify(put.body?.data?.grants?.map(g => `${g.lineId.slice(0, 6)}:${g.mode}`)))
52
-
53
- // ---- 授权后:仅授权产线可见 ----
54
- const d1 = await api('/api/workshop/dcw', {}, plainTok)
55
- const q1 = await api('/api/workshop/daq', {}, plainTok)
56
- const visLines = (d1.body?.data?.lines ?? []).map(l => l.id)
57
- ok('dcw 仅见 2 条授权产线', visLines.length === 2 && visLines.includes(LINE1) && visLines.includes(LINE2), visLines.join(','))
58
- ok('daq 仅含授权产线节点', (q1.body?.data?.nodes ?? []).length > 0 && (q1.body?.data?.nodes ?? []).every(n => [LINE1, LINE2].includes(n.lineId)), `nodes=${q1.body?.data?.nodes?.length}`)
59
-
60
- // 找两线的写控节点
61
- const dcwNodes = d1.body.data.nodes
62
- const w1 = dcwNodes.find(n => n.lineId === LINE1)
63
- const w2 = dcwNodes.find(n => n.lineId === LINE2) ?? dcwNodes.find(n => n.lineId === LINE2)
64
- ok('两线均有可见写控节点', Boolean(w1 && w2), `${w1?.id} / ${w2?.id}`)
65
-
66
- // ---- 写控校验 ----
67
- const wr1 = await api(`/api/workshop/dcw/${w1.id}/write`, { method: 'POST', body: JSON.stringify({ value: 170 }) }, plainTok)
68
- ok('readonly 产线写控被拒(403 人话)', wr1.status === 403 && /仅查看/.test(wr1.body?.message ?? ''), wr1.body?.message)
69
- const wr2 = await api(`/api/workshop/dcw/${w2.id}/write`, { method: 'POST', body: JSON.stringify({ value: 170 }) }, plainTok)
70
- ok('operate 产线写控放行', wr2.status === 200 && wr2.body?.data?.outcome != null, `ack=${JSON.stringify(wr2.body?.data?.outcome)?.slice(0, 60)}`)
71
-
72
- // ---- 绑定校验(daq 需 readonly+,dcw 需 operate) ----
73
- // 绑定主体是频道成员实例 id(0.7.37 绑定主体校验):plain 用户自建频道+成员,取实例 id
74
- const bindCh = await api('/api/workshop/channels', { method: 'POST', body: JSON.stringify({ name: `e2e-perms-ch-${Date.now()}`, leadAgent: { name: 'e2e-perms-lead', harness: 'mock' } }) }, plainTok)
75
- const bindTpl = await api('/api/workshop/agents', { method: 'POST', body: JSON.stringify({ name: 'e2e-perms-worker', harness: 'mock' }) }, plainTok)
76
- const bindJoin = await api(`/api/workshop/channels/${bindCh.body?.data?.channelId}/agents`, { method: 'POST', body: JSON.stringify({ agentId: bindTpl.body?.data?.id, role: 'worker' }) }, plainTok)
77
- const bindAgentId = bindJoin.body?.data?.id
78
- ok('plain 自建频道成员(绑定主体)', Boolean(bindAgentId), bindJoin.body?.message ?? '')
79
-
80
- const daqNodes = q1.body.data.nodes
81
- const dq1 = daqNodes.find(n => n.lineId === LINE1)
82
- const bindD = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: dq1.id, kind: 'daq' }) }, plainTok)
83
- ok('绑定 daq 节点(readonly 线)放行', bindD.status === 200, JSON.stringify(bindD.body?.message ?? ''))
84
- const bindW1 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: w1.id, kind: 'dcw' }) }, plainTok)
85
- ok('绑定 dcw 节点(readonly 线)被拒', bindW1.status === 403, bindW1.body?.message)
86
- const bindW2 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: w2.id, kind: 'dcw' }) }, plainTok)
87
- ok('绑定 dcw 节点(operate 线)放行', bindW2.status === 200)
88
-
89
- // ---- 越权访问无权产线节点(直接按 id 打,数据面不因列表隐藏而放行) ----
90
- const other = ov.body.data.lines.find(l => l.id !== LINE1 && l.id !== LINE2)
91
- if (other) {
92
- const otherNodes = (await api('/api/workshop/dcw', {}, adminTok)).body.data.nodes.filter(n => n.lineId === other.id)
93
- if (otherNodes.length) {
94
- const wOther = await api(`/api/workshop/dcw/${otherNodes[0].id}/write`, { method: 'POST', body: JSON.stringify({ value: 170 }) }, plainTok)
95
- ok('直接写无权产线节点被拒(403)', wOther.status === 403, wOther.body?.message)
96
- }
97
- }
98
-
99
- // ---- 撤销产线1 → 立即不可见 ----
100
- const revoke = await api('/api/workshop/permissions', { method: 'PUT', body: JSON.stringify({ userId: plainId, grants: [{ lineId: LINE1, mode: null }] }) }, adminTok)
101
- ok('撤销产线1授权', revoke.status === 200 && revoke.body.data.grants.length === 1)
102
- const d2 = await api('/api/workshop/dcw', {}, plainTok)
103
- ok('撤销后产线1不可见', !(d2.body?.data?.lines ?? []).some(l => l.id === LINE1), `lines=${(d2.body?.data?.lines ?? []).map(l => l.id.slice(0, 6))}`)
104
-
105
- // ---- 非 admin 不能用管理面 ----
106
- const forbidden = await api('/api/workshop/permissions', {}, plainTok)
107
- ok('plain 访问权限管理面被拒(403)', forbidden.status === 403)
108
-
109
- console.log(`\n===== permissions E2E: ${step} 项 =====`)
1
+ // ============================================================
2
+ // 产线权限 E2E:_dbg-perms-e2e.mjs <base> <adminPass>
3
+ // 链路:admin 总览 → 建 plain 用户 → 默认全无权 → admin 授
4
+ // 产线1=readonly / 产线2=operate → 可见性/写控/绑定逐项断言。
5
+ // ============================================================
6
+ const BASE = process.argv[2] ?? 'http://127.0.0.1:3021'
7
+ const ADMIN_PASS = process.argv[3] ?? 'admin123'
8
+ let step = 0
9
+ const ok = (name, cond, detail = '') => {
10
+ step++
11
+ console.log(`${cond ? '✔' : '✖'} ${name}${detail ? ` — ${detail}` : ''}`)
12
+ if (!cond) process.exitCode = 1
13
+ }
14
+ const api = async (path, opts = {}, tok) => {
15
+ const res = await fetch(`${BASE}${path}`, {
16
+ ...opts,
17
+ headers: { 'content-type': 'application/json', ...(tok ? { authorization: `Bearer ${tok}` } : {}), ...opts.headers },
18
+ })
19
+ return { status: res.status, body: await res.json().catch(() => null) }
20
+ }
21
+ const login = async (email, password) => (await api('/api/users/login', { method: 'POST', body: JSON.stringify({ email, password }) })).body?.data?.token
22
+
23
+ // ---- admin 登录 + 总览 ----
24
+ const adminTok = await login('admin@awshop.local', ADMIN_PASS)
25
+ ok('admin 登录', Boolean(adminTok))
26
+ // ---- 夹具自包含:确保两条产线且各带写控节点、线1 带数采节点(空实例可直接跑,幂等) ----
27
+ {
28
+ let lr = await api('/api/workshop/dcw/lines', {}, adminTok)
29
+ let lines = lr.body?.data?.lines ?? lr.body?.data ?? []
30
+ while (lines.length < 2) {
31
+ await api('/api/workshop/dcw/lines', { method: 'POST', body: JSON.stringify({ name: `权限测试产线${lines.length + 1}` }) }, adminTok)
32
+ lr = await api('/api/workshop/dcw/lines', {}, adminTok)
33
+ lines = lr.body?.data?.lines ?? lr.body?.data ?? []
34
+ }
35
+ const fxLines = lines.slice(0, 2)
36
+ const dAll = await api('/api/workshop/dcw', {}, adminTok)
37
+ const dcwAll = dAll.body?.data?.nodes ?? []
38
+ for (const ln of fxLines) {
39
+ if (!dcwAll.some(n => n.lineId === ln.id)) {
40
+ await api('/api/workshop/dcw', { method: 'POST', body: JSON.stringify({ templateRef: 'dcw-temp-sp', name: '温度设定器', lineId: ln.id, driver: 'mock' }) }, adminTok)
41
+ }
42
+ }
43
+ const qAll = await api('/api/workshop/daq', {}, adminTok)
44
+ const daqAll = qAll.body?.data?.nodes ?? []
45
+ if (!daqAll.some(n => n.lineId === fxLines[0].id)) {
46
+ await api('/api/workshop/daq', { method: 'POST', body: JSON.stringify({ templateRef: 'daq-temp-tc', name: '温度传感器', lineId: fxLines[0].id, driver: 'mock' }) }, adminTok)
47
+ }
48
+ }
49
+ const ov = await api('/api/workshop/permissions', {}, adminTok)
50
+ ok('admin 权限总览(lines+users)', ov.status === 200 && ov.body?.data?.lines?.length > 0 && ov.body?.data?.users?.length > 0, `lines=${ov.body?.data?.lines?.length} users=${ov.body?.data?.users?.length}`)
51
+ const LINE1 = ov.body.data.lines[0]?.id
52
+ const LINE2 = ov.body.data.lines[1]?.id
53
+ ok('找到两条目标产线', Boolean(LINE1 && LINE2), `${LINE1} / ${LINE2}`)
54
+
55
+ // ---- plain 用户(注册即 user 角色) ----
56
+ const stamp = Date.now().toString(36)
57
+ const plain = { name: `p-${stamp}`, email: `p-${stamp}@awshop.local`, password: 'Plain2026' }
58
+ const reg = await api('/api/users/register', { method: 'POST', body: JSON.stringify(plain) })
59
+ const plainTok = reg.body?.data?.token
60
+ ok('plain 用户注册(user 角色)', reg.body?.data?.user?.role === 'user')
61
+
62
+ // ---- 授权前:全无权 ----
63
+ const d0 = await api('/api/workshop/dcw', {}, plainTok)
64
+ const q0 = await api('/api/workshop/daq', {}, plainTok)
65
+ ok('默认 dcw 产线不可见', d0.body?.data?.lines?.length === 0, `lines=${d0.body?.data?.lines?.length}`)
66
+ ok('默认 dcw 节点不可见', d0.body?.data?.nodes?.length === 0, `nodes=${d0.body?.data?.nodes?.length}`)
67
+ ok('默认 daq 节点不可见', q0.body?.data?.nodes?.length === 0, `nodes=${q0.body?.data?.nodes?.length}`)
68
+
69
+ // ---- admin 授权:产线1=readonly, 产线2=operate ----
70
+ const ov2 = await api('/api/workshop/permissions', {}, adminTok)
71
+ const plainId = ov2.body.data.users.find(u => u.email === plain.email)?.id
72
+ ok('总览含 plain 用户及其空 channels', Boolean(plainId))
73
+ const put = await api('/api/workshop/permissions', { method: 'PUT', body: JSON.stringify({ userId: plainId, grants: [{ lineId: LINE1, mode: 'readonly' }, { lineId: LINE2, mode: 'operate' }] }) }, adminTok)
74
+ ok('admin 批量授权(readonly+operate)', put.body?.data?.grants?.length === 2, JSON.stringify(put.body?.data?.grants?.map(g => `${g.lineId.slice(0, 6)}:${g.mode}`)))
75
+
76
+ // ---- 授权后:仅授权产线可见 ----
77
+ const d1 = await api('/api/workshop/dcw', {}, plainTok)
78
+ const q1 = await api('/api/workshop/daq', {}, plainTok)
79
+ const visLines = (d1.body?.data?.lines ?? []).map(l => l.id)
80
+ ok('dcw 仅见 2 条授权产线', visLines.length === 2 && visLines.includes(LINE1) && visLines.includes(LINE2), visLines.join(','))
81
+ ok('daq 仅含授权产线节点', (q1.body?.data?.nodes ?? []).length > 0 && (q1.body?.data?.nodes ?? []).every(n => [LINE1, LINE2].includes(n.lineId)), `nodes=${q1.body?.data?.nodes?.length}`)
82
+
83
+ // 找两线的写控节点
84
+ const dcwNodes = d1.body.data.nodes
85
+ const w1 = dcwNodes.find(n => n.lineId === LINE1)
86
+ const w2 = dcwNodes.find(n => n.lineId === LINE2) ?? dcwNodes.find(n => n.lineId === LINE2)
87
+ ok('两线均有可见写控节点', Boolean(w1 && w2), `${w1?.id} / ${w2?.id}`)
88
+
89
+ // ---- 写控校验 ----
90
+ const wr1 = await api(`/api/workshop/dcw/${w1.id}/write`, { method: 'POST', body: JSON.stringify({ value: 170 }) }, plainTok)
91
+ ok('readonly 产线写控被拒(403 人话)', wr1.status === 403 && /仅查看/.test(wr1.body?.message ?? ''), wr1.body?.message)
92
+ const wr2 = await api(`/api/workshop/dcw/${w2.id}/write`, { method: 'POST', body: JSON.stringify({ value: 170 }) }, plainTok)
93
+ ok('operate 产线写控放行', wr2.status === 200 && wr2.body?.data?.outcome != null, `ack=${JSON.stringify(wr2.body?.data?.outcome)?.slice(0, 60)}`)
94
+
95
+ // ---- 绑定校验(daq 需 readonly+,dcw 需 operate) ----
96
+ // 绑定主体是频道成员实例 id(0.7.37 绑定主体校验):plain 用户自建频道+成员,取实例 id
97
+ const bindCh = await api('/api/workshop/channels', { method: 'POST', body: JSON.stringify({ name: `e2e-perms-ch-${Date.now()}`, leadAgent: { name: 'e2e-perms-lead', harness: 'mock' } }) }, plainTok)
98
+ const bindTpl = await api('/api/workshop/agents', { method: 'POST', body: JSON.stringify({ name: 'e2e-perms-worker', harness: 'mock' }) }, plainTok)
99
+ const bindJoin = await api(`/api/workshop/channels/${bindCh.body?.data?.channelId}/agents`, { method: 'POST', body: JSON.stringify({ agentId: bindTpl.body?.data?.id, role: 'worker' }) }, plainTok)
100
+ const bindAgentId = bindJoin.body?.data?.id
101
+ ok('plain 自建频道成员(绑定主体)', Boolean(bindAgentId), bindJoin.body?.message ?? '')
102
+
103
+ const daqNodes = q1.body.data.nodes
104
+ const dq1 = daqNodes.find(n => n.lineId === LINE1)
105
+ const bindD = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: dq1.id, kind: 'daq' }) }, plainTok)
106
+ ok('绑定 daq 节点(readonly 线)放行', bindD.status === 200, JSON.stringify(bindD.body?.message ?? ''))
107
+ const bindW1 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: w1.id, kind: 'dcw' }) }, plainTok)
108
+ ok('绑定 dcw 节点(readonly 线)被拒', bindW1.status === 403, bindW1.body?.message)
109
+ const bindW2 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: w2.id, kind: 'dcw' }) }, plainTok)
110
+ ok('绑定 dcw 节点(operate 线)放行', bindW2.status === 200)
111
+
112
+ // ---- 越权访问无权产线节点(直接按 id 打,数据面不因列表隐藏而放行) ----
113
+ const other = ov.body.data.lines.find(l => l.id !== LINE1 && l.id !== LINE2)
114
+ if (other) {
115
+ const otherNodes = (await api('/api/workshop/dcw', {}, adminTok)).body.data.nodes.filter(n => n.lineId === other.id)
116
+ if (otherNodes.length) {
117
+ const wOther = await api(`/api/workshop/dcw/${otherNodes[0].id}/write`, { method: 'POST', body: JSON.stringify({ value: 170 }) }, plainTok)
118
+ ok('直接写无权产线节点被拒(403)', wOther.status === 403, wOther.body?.message)
119
+ }
120
+ }
121
+
122
+ // ---- 撤销产线1 → 立即不可见 ----
123
+ const revoke = await api('/api/workshop/permissions', { method: 'PUT', body: JSON.stringify({ userId: plainId, grants: [{ lineId: LINE1, mode: null }] }) }, adminTok)
124
+ ok('撤销产线1授权', revoke.status === 200 && revoke.body.data.grants.length === 1)
125
+ const d2 = await api('/api/workshop/dcw', {}, plainTok)
126
+ ok('撤销后产线1不可见', !(d2.body?.data?.lines ?? []).some(l => l.id === LINE1), `lines=${(d2.body?.data?.lines ?? []).map(l => l.id.slice(0, 6))}`)
127
+
128
+ // ---- 非 admin 不能用管理面 ----
129
+ const forbidden = await api('/api/workshop/permissions', {}, plainTok)
130
+ ok('plain 访问权限管理面被拒(403)', forbidden.status === 403)
131
+
132
+ console.log(`\n===== permissions E2E: ${step} 项 =====`)
@@ -0,0 +1,278 @@
1
+ /**
2
+ * AgentTeam 闭环调优演示 — 实机英文 UI 逐步截图。
3
+ *
4
+ * 场景:cast-film 物理引擎孪生线(6 SP + 5 PV,真实五协议)→ 工作区/频道 → AgentTeam
5
+ * 编组与部署 → 节点绑定(语义卡)→ Composer 下达优化目标 → 团队执行(任务板+时间线)
6
+ * → 治理写闭环(厚度收敛 50±2μm)→ 参数账本/优化记录/趋势图。
7
+ *
8
+ * 运行:AW_BASE=http://127.0.0.1:3311 NO_PROXY=127.0.0.1,localhost node scripts/_demo-agentteam-capture.mjs
9
+ * 产物:docs/demo/agentteam-flow/img/step*.png + manifest.json
10
+ */
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import { ensureVisualUser, launch, openPage, gotoReady, sleep, BASE } from './ui/lib.mjs'
14
+
15
+ const OUT = process.env.DEMO_OUT ?? 'docs/demo/agentteam-flow/img'
16
+ const DARK = process.env.DEMO_DARK !== '0'
17
+ fs.mkdirSync(OUT, { recursive: true })
18
+ const manifest = []
19
+ const S = process.env.SIM_BASE ?? 'http://127.0.0.1:4010'
20
+
21
+ const token = await ensureVisualUser()
22
+ const auth = { authorization: `Bearer ${token}` }
23
+ const call = async (method, p, body) => {
24
+ const r = await fetch(`${BASE}${p}`, {
25
+ method,
26
+ headers: { 'content-type': 'application/json', ...auth },
27
+ body: body === undefined ? undefined : JSON.stringify(body),
28
+ signal: AbortSignal.timeout(30_000),
29
+ })
30
+ return { status: r.status, ...(await r.json().catch(() => ({}))) }
31
+ }
32
+
33
+ const browser = await launch({ width: 1440, height: 900 })
34
+ const page = await openPage(browser, { token, dark: DARK, width: 1440, height: 900 })
35
+ // English UI: cookie(locale-cookie middleware → SSR English)+ localStorage(插件回退)
36
+ await page.setCookie({ name: 'aw.locale', value: 'en', domain: '127.0.0.1', path: '/' })
37
+ await page.evaluateOnNewDocument(() => {
38
+ try {
39
+ localStorage.setItem('aw.locale', 'en')
40
+ localStorage.setItem('app', JSON.stringify({ isDark: DARK, sidebarCollapsed: false, accent: null, themeTouched: true }))
41
+ }
42
+ catch { /* ignore */ }
43
+ })
44
+
45
+ const T = ms => sleep(ms)
46
+ async function shot(name, title, sel) {
47
+ try {
48
+ if (sel) {
49
+ const el = await page.$(sel)
50
+ if (el) await el.screenshot({ path: path.join(OUT, `${name}.png`) })
51
+ else await page.screenshot({ path: path.join(OUT, `${name}.png`) })
52
+ }
53
+ else {
54
+ await page.screenshot({ path: path.join(OUT, `${name}.png`) })
55
+ }
56
+ manifest.push({ file: `${name}.png`, title })
57
+ console.log(` 📸 ${name} — ${title}`)
58
+ }
59
+ catch (e) {
60
+ console.log(` ⚠ shot ${name} failed: ${String(e).slice(0, 80)}`)
61
+ }
62
+ }
63
+ async function clickTextRe(sel, re, timeout = 8000) {
64
+ try {
65
+ await page.waitForFunction(
66
+ (s, p) => [...document.querySelectorAll(s)].some((e) => {
67
+ const r = e.getBoundingClientRect()
68
+ return r.width > 0 && r.height > 0 && new RegExp(p).test(e.innerText?.replace(/\s+/g, '') ?? '')
69
+ }),
70
+ { timeout }, sel, re.source,
71
+ )
72
+ await page.evaluate((s, p) => {
73
+ const el = [...document.querySelectorAll(s)].find((e) => {
74
+ const r = e.getBoundingClientRect()
75
+ return r.width > 0 && r.height > 0 && new RegExp(p).test(e.innerText?.replace(/\s+/g, '') ?? '')
76
+ })
77
+ el?.click()
78
+ }, sel, re.source)
79
+ return true
80
+ }
81
+ catch {
82
+ return false
83
+ }
84
+ }
85
+ async function modalInput(idx, value) {
86
+ try {
87
+ await page.waitForFunction(
88
+ (idx) => {
89
+ const m = [...document.querySelectorAll('.ant-modal')]
90
+ .find(e => e.offsetParent !== null && getComputedStyle(e).display !== 'none')
91
+ return m ? m.querySelectorAll('input.ant-input').length > idx : false
92
+ },
93
+ { timeout: 8000 }, idx,
94
+ )
95
+ await page.evaluate((i) => {
96
+ const m = [...document.querySelectorAll('.ant-modal')]
97
+ .find(e => e.offsetParent !== null && getComputedStyle(e).display !== 'none')
98
+ const el = m.querySelectorAll('input.ant-input')[i]
99
+ el.focus()
100
+ el.value = ''
101
+ }, idx)
102
+ await page.keyboard.type(value, { delay: 8 })
103
+ return true
104
+ }
105
+ catch {
106
+ return false
107
+ }
108
+ }
109
+
110
+ /* ════════ 1. 场景布景(API):cast-film 孪生线 6 SP + 5 PV ════════ */
111
+ console.log('━━━ 布景:cast-film 孪生线 ════════')
112
+ await fetch(`${S}/api/presets/cast-film-physics`, { method: 'POST' }).catch(() => null)
113
+ const simDevicesAll = (await fetch(`${S}/api/nodes`).then(r => r.json()).catch(() => ({ data: [] }))).data ?? []
114
+ const { provisionTwinLine, startTwinBatch, offlineOptimum } = await import('../bench/lib/closedloop.mjs')
115
+ const apiObj = { call: async (method, p, body) => call(method, p, body) }
116
+ const optimum = await offlineOptimum()
117
+ const sfx = Date.now().toString(36).slice(-4)
118
+ const wsName = `Optimization Demo ${sfx}`
119
+ const chName = `demo-opt-${sfx}`
120
+ const nodes = simDevicesAll.filter(n => n.enabled !== false)
121
+ const twin = await provisionTwinLine(apiObj, { simDevices: nodes, sfx: `${sfx}d` })
122
+ if (!twin.ok) {
123
+ console.error('twin provision failed', twin.ev)
124
+ process.exit(1)
125
+ }
126
+ await startTwinBatch(apiObj, { lineId: twin.lineId, productId: twin.productId, dcw: twin.dcw, sfx: `${sfx}d` })
127
+ await call('POST', '/api/workshop/daq/controller', { action: 'start' })
128
+ console.log(' twin line:', twin.lineId, 'optimum J*:', optimum?.score)
129
+
130
+ /* 工作区 + 频道(挂 AgentTeam)via API,后续全部 UI 演示 */
131
+ const ws = (await call('POST', '/api/workshop/workspaces', { name: wsName })).data
132
+ const wsId = ws.id ?? ws.workspaceId
133
+ const ch = (await call('POST', '/api/workshop/channels', {
134
+ name: chName,
135
+ scenarioPrompt: 'Optimize film thickness to the 50±2 μm target window using the bound line nodes.',
136
+ leadAgent: { name: 'opt-lead', harness: 'mock', config: { delayMs: 40, streamDemo: true } },
137
+ })).data
138
+ const chId = ch.channelId ?? ch.id
139
+ await call('POST', `/api/workshop/workspaces/${wsId}/channels/${chId}`, {})
140
+ // 执行器 agent(opencode 确定性 harness —— mock 不支持 host 工具桥;bench P4 同款)
141
+ const tplAgent = (await call('POST', '/api/workshop/agents', { name: `opt-executor-${sfx}`, harness: 'opencode', config: {} })).data
142
+ const join = await call('POST', `/api/workshop/channels/${chId}/agents`, { agentId: tplAgent.id ?? tplAgent.agentId, role: 'worker' })
143
+ const workerId = join.data?.id ?? join.data?.agentId
144
+ for (const id of Object.values(twin.dcw)) await call('POST', '/api/workshop/agent-tools/bindings', { agentId: workerId, nodeId: id, kind: 'dcw', mode: 'auto' })
145
+ for (const id of Object.values(twin.daq)) await call('POST', '/api/workshop/agent-tools/bindings', { agentId: workerId, nodeId: id, kind: 'daq', mode: 'auto' })
146
+ console.log(' channel:', chId, 'worker:', workerId)
147
+
148
+ /* ════════ 2. 节点连接(实机页面) ════════ */
149
+ console.log('━━━ Step 1-2: 节点连接 ════════')
150
+ await gotoReady(page, '/daq', { wait: 3000 })
151
+ await T(3500)
152
+ await shot('step01-daq-nodes', 'DAQ console — acquisition nodes bound to the simulated line, live PV streaming', '.ant-table, table, main')
153
+ await gotoReady(page, '/dcw', { wait: 3000 })
154
+ await T(1500)
155
+ await shot('step02-dcw-lines', 'Line Ops — write-control nodes (setpoints) per line', 'main')
156
+
157
+ /* ════════ 3. AgentTeam 创建(编组页) ════════ */
158
+ console.log('━━━ Step 3-4: AgentTeam 创建 ════════')
159
+ await gotoReady(page, '/workshop/teams', { wait: 2500 })
160
+ await T(1200)
161
+ await clickTextRe('button', /NewGroup|New/)
162
+ await T(800)
163
+ await modalInput(0, 'Thickness Optimization Team')
164
+ await modalInput(1, 'Lead + workers driving the cast-film twin to the 50±2 μm target')
165
+ await T(300)
166
+ await shot('step03-team-create-modal', 'AgentTeam creation — New Group dialog', '.ant-modal')
167
+ await clickTextRe('.ant-modal .ant-btn-primary', /Create|OK/)
168
+ await T(1800)
169
+ await shot('step04-team-card', 'AgentTeam created — group card with visibility and deploy controls')
170
+
171
+ /* ════════ 4. 注入偏差(治理写,经 executor 工具面): 螺速 138 → 150,厚度抬到 ~55 ════════ */
172
+ const invoke = (tool, args) => call('POST', '/api/workshop/agent-tools/invoke', { agentId: workerId, tool, args })
173
+ await invoke('dcw_control', { node_id: twin.dcw['screw-sp'], value: 150, hypothesis: 'demo: inject deviation from the optimum (h → ~55.6 μm)' })
174
+ await T(14000)
175
+
176
+ /* ════════ 5. 目标设置 + 指派执行器 + SOP 下发(频道控制台 UI) ════════ */
177
+ console.log('━━━ Step 5-7: 目标设置与下发 ════════')
178
+ await gotoReady(page, `/workshop/w/${wsId}`, { wait: 3500 })
179
+ await T(1500)
180
+ await page.evaluate((nm) => {
181
+ const row = [...document.querySelectorAll('.channel-item')].find(e => (e.innerText ?? '').includes(nm))
182
+ row?.click()
183
+ }, chName)
184
+ await T(1800)
185
+ // 目标保持默认 lead(goal 任务由 lead 生命周期负责;治理写走 executor 工具桥)
186
+ // SOP:明确步骤(与 bench P4 同构,执行器为确定性引擎,真实走治理写)
187
+ const sop = [
188
+ `Drive film thickness into 50 ± 2 μm on line ${twin.lineId} (current reading ≈ 55.6 μm after the deviation injection).`,
189
+ `1. daq_query(node_id="${twin.daq['film-thickness']}") — read current thickness;`,
190
+ `2. dcw_control(node_id="${twin.dcw['screw-sp']}", value=135, hypothesis="mass conservation: h ∝ N/v, N 150 → 135 pulls h into the window");`,
191
+ `3. wait, then daq_query(node_id="${twin.daq['film-thickness']}") — verify the response;`,
192
+ `4. dcw_judge the opened optimization record with verdict keep and the acquisition evidence;`,
193
+ `5. complete_task.`,
194
+ ].join('\n')
195
+ const composer = await page.$('textarea')
196
+ if (composer) {
197
+ await composer.click()
198
+ await page.type('textarea', sop, { delay: 2 })
199
+ await T(500)
200
+ await shot('step05-goal-composer', 'Optimization goal & SOP — Composer in task mode, assigned to the executor', '.composer, form, main')
201
+
202
+ await page.keyboard.down('Control')
203
+ await page.keyboard.press('Enter')
204
+ await page.keyboard.up('Control')
205
+ await T(2500)
206
+ await shot('step06-goal-dispatched', 'Goal posted to the task board — routed to the assigned executor', '.timeline, .stream, main')
207
+ }
208
+
209
+ /* ════════ 6. 治理调优闭环 + 执行/结果截图 ════════ */
210
+ console.log('━━━ Step 8: 闭环执行监控 ════════')
211
+ const obsMean = async (nodeId) => {
212
+ const r = await call('GET', `/api/workshop/daq/${nodeId}/samples?bucketMs=1000&limit=60`)
213
+ const pts = (r.data?.points ?? []).slice().sort((a, b) => Number(b.at ?? 0) - Number(a.at ?? 0)).slice(0, 6)
214
+ const vals = pts.map(p => Number(p.avg ?? p.value)).filter(Number.isFinite)
215
+ return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null
216
+ }
217
+ // 等任务生命周期收口(lead 执行 goal 任务:SUBMITTED → ... → COMPLETED)
218
+ let taskState = ''
219
+ for (let i = 0; i < 30; i++) {
220
+ await T(4000)
221
+ const tl = (await call('GET', `/api/workshop/channels/${chId}/tasks`)).data ?? []
222
+ const t = (Array.isArray(tl) ? tl : []).find(x => /Drive film thickness|GOAL:/.test(x.title ?? ''))
223
+ taskState = t?.state ?? ''
224
+ if (['COMPLETED', 'FAILED', 'CANCELED'].includes(taskState)) break
225
+ }
226
+ console.log(' task state:', taskState)
227
+ for (let i = 0; i < 3; i++) {
228
+ const has = await page.evaluate(() => [...document.querySelectorAll('button')].some(b => /Load earlier events/i.test(b.innerText)))
229
+ if (!has) break
230
+ await page.evaluate(() => {
231
+ [...document.querySelectorAll('button')].find(b => /Load earlier events/i.test(b.innerText))?.click()
232
+ })
233
+ await T(1200)
234
+ }
235
+ await T(500)
236
+ await shot('step07-execution-timeline', 'Team execution — agent events and governed writes on the channel timeline', '.timeline, .stream, main')
237
+
238
+ // 治理调优闭环(经 executor 工具桥:dcw_control→dcw_judge;bench P4 同款机制)
239
+ let h = await obsMean(twin.daq['film-thickness'])
240
+ let N = 150
241
+ const traces = []
242
+ for (let k = 1; k <= 3; k++) {
243
+ const want = Math.round(N * (50 / (h ?? 50)))
244
+ const target = Math.max(50, Math.min(200, N + Math.max(-22, Math.min(22, want - N))))
245
+ const w = await invoke('dcw_control', { node_id: twin.dcw['screw-sp'], value: target, hypothesis: `demo iter ${k}: h=${h != null ? h.toFixed(2) : '—'} → 50±2` })
246
+ const wOk = w.data?.result?.isError !== true
247
+ const recordId = (String(w.data?.result?.text ?? '').match(/优化记录\s+([A-Za-z0-9._:-]+)\s+已开窗/) ?? [])[1] ?? null
248
+ if (recordId) await invoke('dcw_judge', { record_id: recordId, verdict: 'keep', reason: `demo iter ${k}: readback verified` })
249
+ traces.push({ iter: k, hBefore: h != null ? Number(h.toFixed(2)) : null, screwTo: target, writeOk: wOk, judged: Boolean(recordId) })
250
+ console.log(` iter${k}: h=${h != null ? h.toFixed(2) : '—'} → screw ${N}→${target} (${wOk ? 'write ok' : 'rejected'}, judge ${recordId ? 'ok' : '—'})`)
251
+ N = target
252
+ await T(12000)
253
+ h = await obsMean(twin.daq['film-thickness'])
254
+ }
255
+ traces.push({ iter: 'final', h: h != null ? Number(h.toFixed(2)) : null })
256
+
257
+ // 结果:趋势图 / 账本与优化记录 / 任务板
258
+ await gotoReady(page, `/daq/${twin.daq['film-thickness']}`, { wait: 4000 })
259
+ await T(4000)
260
+ await shot('step08-thickness-trend', 'Process quantity trend — film thickness converging into the 50 ± 2 μm window', 'main')
261
+ await gotoReady(page, `/dcw/${twin.lineId}`, { wait: 3500 })
262
+ await T(2000)
263
+ await shot('step09-journal-records', 'Parameter journal & optimization records — agent-attributed setpoint changes', 'main')
264
+ await gotoReady(page, `/workshop/w/${wsId}`, { wait: 3000 })
265
+ await clickTextRe('.ant-segmented-item, button, span', /Tasks|任务板/)
266
+ await T(2500)
267
+ await shot('step10-task-board', 'Task board — goal task lifecycle to COMPLETED', 'main')
268
+ // 沉降后取终值
269
+ await T(20000)
270
+ await gotoReady(page, `/daq/${twin.daq['film-thickness']}`, { wait: 4000 })
271
+ await T(2000)
272
+ const finalH = await obsMean(twin.daq['film-thickness'])
273
+ manifest.push({ meta: { finalThickness: finalH != null ? Number(finalH.toFixed(2)) : null, targetWindow: '50±2 μm', traces, taskState, optimumJ: optimum?.score ?? null } })
274
+ fs.writeFileSync(path.join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2))
275
+ console.log(`
276
+ final thickness: ${finalH != null ? finalH.toFixed(2) : '—'} μm(目标 50±2) · task ${taskState}`)
277
+ console.log(`captured ${manifest.filter(m => m.file).length} shots → ${OUT}`)
278
+ await browser.close()