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,661 +1,608 @@
1
- /**
2
- * diag-bridge —— industrial-deep-diagnostic 深度诊断服务桥接插件。
3
- *
4
- * 把诊断服务(http://127.0.0.1:3210)接入 AgentWorkShop:导出 DAQ 时序快照 CSV →
5
- * 上传并发起深度诊断 → 轮询状态 → 完成后报告自动入知识库(文档/索引/经验三步)。
6
- * 工具对 lead/worker 全体 agent 生效;3210 不可达时一律 isError 文本,绝不抛异常。
7
- */
8
- const MIN = 60 * 1000
9
- const DEFAULT_BASE = 'http://127.0.0.1:3210'
10
- const SNAPSHOT_BUCKET_MS = 5000 // 快照降采样桶宽
11
- const SNAPSHOT_LIMIT = 10000 // 与 ctx.daq.query limit 一致(截断断言阈值)
12
- const UPLOAD_FOLDER = 'aw-snapshots'
13
- const AUTO_COOLDOWN_MS = 30 * MIN // 自动诊断同产线冷却
14
-
15
- /** 每产线「启动中」互斥(进程内同步集合,堵住 kv 检查与远端落库之间的并发窗口) */
16
- const startingLines = new Set()
17
-
18
- // ── 基础工具 ──────────────────────────────────────────────────────────────
19
-
20
- /** 诊断服务 base(kv diag.base_url;只允许 http/https 且 host 为 127.0.0.1/localhost) */
21
- function baseOf(ctx) {
22
- const raw = String(ctx.config?.get?.('plugins.diag-bridge.base_url') || ctx.kv.get('diag.base_url') || DEFAULT_BASE).trim()
23
- try {
24
- const u = new URL(raw)
25
- const okProto = u.protocol === 'http:' || u.protocol === 'https:'
26
- const okHost = u.hostname === '127.0.0.1' || u.hostname === 'localhost'
27
- if (!okProto || !okHost) return null
28
- return raw.replace(/\/+$/, '')
29
- }
30
- catch {
31
- return null
32
- }
33
- }
34
-
35
- /** 诊断服务鉴权 token(系统配置 plugins.diag-bridge.token 优先,kv diag.token 兜底;空=匿名)。
36
- * 诊断服务 v4 起默认强制 Bearer(AUTH_ENABLED!==0),401 AUTH_REQUIRED 时先查这里。 */
37
- function diagTokenOf(ctx) {
38
- return String(ctx.config?.get?.('plugins.diag-bridge.token') || ctx.kv.get('diag.token') || '').trim()
39
- }
40
-
41
- function authHeadersOf(ctx) {
42
- const t = diagTokenOf(ctx)
43
- return t ? { authorization: `Bearer ${t}` } : {}
44
- }
45
-
46
- /** rag-knowledge 出站鉴权(系统配置 plugins.rag-bridge.token 优先,kv kb.token;与 rag-bridge 同源。
47
- * 入库管线要打 web 6789 / api 8770,服务端开启鉴权时缺头会 401 → catalog 查不到库 → 跳过入库) */
48
- function kbHeadersOf(ctx) {
49
- const t = String(ctx.config?.get?.('plugins.rag-bridge.token') || ctx.kv.get('kb.token') || '').trim()
50
- return t ? { 'authorization': `Bearer ${t}`, 'x-kb-token': t } : {}
51
- }
52
-
53
- // ── 配置读取(系统配置优先,kv 兜底;前端全局设置→运行配置→插件组可改,保存即热生效) ──
54
-
55
- const harnessOf = ctx => String(ctx.config?.get?.('plugins.diag-bridge.harness') || ctx.kv.get('diag.harness') || 'omp')
56
- const maxTurnsOf = ctx => Number(ctx.config?.get?.('plugins.diag-bridge.max_turns')) || Number(ctx.kv.get('diag.max_turns')) || 220
57
- const maxMinutesOf = ctx => Number(ctx.config?.get?.('plugins.diag-bridge.max_minutes')) || Number(ctx.kv.get('diag.max_minutes')) || 40
58
- /** 自动诊断开关:系统配置布尔或 kv 'true'(kv 为历史字符串语义) */
59
- const autoEnabledOf = ctx => ctx.config?.get?.('plugins.diag-bridge.auto_enabled') === true || ctx.kv.get('auto_diag_enabled') === 'true'
60
- const autoRulesOf = ctx => String(ctx.config?.get?.('plugins.diag-bridge.auto_rules') || ctx.kv.get('auto_rules') || '')
61
-
62
- const runKey = id => `run:${id}`
63
-
64
- /** kv 中全部 run 记录 → [{ id, meta }] */
65
- function kvRuns(ctx) {
66
- const out = []
67
- for (const [k, v] of Object.entries(ctx.kv.all())) {
68
- if (k.startsWith('run:') && v && typeof v === 'object') out.push({ id: k.slice(4), meta: v })
69
- }
70
- return out
71
- }
72
-
73
- /** 该产线是否有运行中的诊断 */
74
- function runningOfLine(ctx, line) {
75
- return kvRuns(ctx).find(r => r.meta?.status === 'running' && r.meta?.line === line) ?? null
76
- }
77
-
78
- /** 短 JSON 摘要(错误信息用) */
79
- function short(body) {
80
- try {
81
- return JSON.stringify(body).slice(0, 200)
82
- }
83
- catch {
84
- return String(body)
85
- }
86
- }
87
-
88
- /** 401 时给可操作的修复指引(生产最常见故障:会话 token 随诊断服务重启失效) */
89
- function authHint(status, msg) {
90
- if (status !== 401) return msg
91
- return `${msg};修复:在 系统设置→运行配置→插件 更新「诊断 API Token」(推荐使用诊断服务侧的持久化 API Token,idd_ 前缀,服务重启不失效)`
92
- }
93
-
94
- /** JSON GET:断言 HTTP ok 且 body.success===true,返回 body */
95
- async function jget(ctx, url, timeoutMs = 8000) {
96
- const res = await ctx.http.get(url, { timeoutMs, headers: authHeadersOf(ctx) })
97
- const body = await res.json().catch(() => null)
98
- if (!res.ok) throw new Error(authHint(res.status, `HTTP ${res.status} ${short(body)}`))
99
- if (!body || body.success !== true) throw new Error(`success!=true ${short(body)}`)
100
- return body
101
- }
102
-
103
- /** JSON POST:断言 HTTP ok 且 body.success===true,返回 body(extraHeaders 供 KB 端点叠加鉴权) */
104
- async function jpost(ctx, url, body, timeoutMs = 15000, extraHeaders = {}) {
105
- const res = await ctx.http.post(url, body, { timeoutMs, headers: { ...authHeadersOf(ctx), ...extraHeaders } })
106
- const out = await res.json().catch(() => null)
107
- if (!res.ok) throw new Error(authHint(res.status, `HTTP ${res.status} ${short(out)}`))
108
- if (!out || out.success !== true) throw new Error(`success!=true ${short(out)}`)
109
- return out
110
- }
111
-
112
- /** 解析 auto_rules(kv 存 JSON 字符串 {nodeId:{op:'gt'|'lt',value}}) */
113
- function parseAutoRules(raw) {
114
- if (!raw) return {}
115
- try {
116
- const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
117
- return obj && typeof obj === 'object' ? obj : {}
118
- }
119
- catch {
120
- return {}
121
- }
122
- }
123
-
124
- /** CSV 单元格转义(含逗号/引号/换行时加引号) */
125
- function csvCell(v) {
126
- const s = String(v)
127
- return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
128
- }
129
-
130
- /** 数值格式化(保留 6 位小数,去尾零;非有限值 → 空) */
131
- function fmtNum(v) {
132
- if (typeof v !== 'number' || !Number.isFinite(v)) return ''
133
- return String(Math.round(v * 1e6) / 1e6)
134
- }
135
-
136
- /** 快照文件名(ASCII 安全,避免 multipart filename 编码问题) */
137
- function safeName(line, toMs) {
138
- const s = String(line).replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40) || 'line'
139
- return `snap-${s}-${toMs}.csv`
140
- }
141
-
142
- // ── 快照导出器 ───────────────────────────────────────────────────────────
143
-
144
- /**
145
- * 导出产线时窗快照:daq 节点过滤 → 时序查询 → 时间戳 pivot → CSV → multipart 上传。
146
- * 成功返回 { ok:true, csvPath, rows, nodes, fromMs, toMs };失败返回 { ok:false, error }。
147
- */
148
- async function snapshotCore(ctx, line, fromMs, toMs) {
149
- if (!ctx.daq || typeof ctx.daq.nodes !== 'function' || typeof ctx.daq.query !== 'function') {
150
- return { ok: false, error: '平台 ctx.daq 扩展未就绪(需宿主提供 daq.nodes / daq.query)。' }
151
- }
152
- // 1) 产线节点过滤(节点产线归属字段为 lineId,daq-node.repo snapshot 同构)
153
- const all = await ctx.daq.nodes()
154
- const lineNodes = (Array.isArray(all) ? all : []).filter(n => n && n.lineId === line)
155
- if (!lineNodes.length) return { ok: false, error: `产线 ${line} 下无数采节点。` }
156
-
157
- // 2) 时序查询(bucket 点含 {at,avg,min,max,cnt},raw 点含 {at,value});
158
- // 打标查询空窗(LineRun 未开跑/空闲段样本无 line_id)时降级为按节点查询
159
- let map = await ctx.daq.query({
160
- lineId: line,
161
- nodeIds: lineNodes.map(n => n.id),
162
- fromMs,
163
- toMs,
164
- bucketMs: SNAPSHOT_BUCKET_MS,
165
- limit: SNAPSHOT_LIMIT,
166
- })
167
- if (!(map instanceof Map) || map.size === 0) {
168
- map = await ctx.daq.query({
169
- nodeIds: lineNodes.map(n => n.id),
170
- fromMs,
171
- toMs,
172
- bucketMs: SNAPSHOT_BUCKET_MS,
173
- limit: SNAPSHOT_LIMIT,
174
- })
175
- }
176
-
177
- // 3) pivot:收集全部时间戳排序去重;行=时间戳,列=每节点(bucket 取 avg,raw 取 value)
178
- const tsSet = new Set()
179
- const perNode = new Map() // nodeId → Map(at → value)
180
- for (const [nodeId, points] of (map instanceof Map ? map : [])) {
181
- if (!Array.isArray(points)) continue
182
- const m = new Map()
183
- for (const p of points) {
184
- if (!p || typeof p.at !== 'number') continue
185
- const v = typeof p.avg === 'number' ? p.avg : (typeof p.value === 'number' ? p.value : null)
186
- if (v == null) continue
187
- m.set(p.at, v)
188
- tsSet.add(p.at)
189
- }
190
- if (m.size) perNode.set(nodeId, m)
191
- }
192
- const stamps = [...tsSet].sort((a, b) => a - b)
193
- if (!stamps.length) return { ok: false, error: `产线 ${line} 在该时窗内无数采样本,请调整时间窗口。` }
194
- if (stamps.length >= SNAPSHOT_LIMIT) {
195
- return { ok: false, error: `唯一时间戳数(${stamps.length})≥ 上限 ${SNAPSHOT_LIMIT},数据可能被 limit 截断,请缩小时间窗口后重试。` }
196
- }
197
-
198
- // 4) 组 CSV(timestamp 用平台本地时区 ISO;缺值留空)
199
- const cols = lineNodes.map(n => n.id)
200
- const rowsCsv = ['timestamp,' + cols.map(csvCell).join(',')]
201
- for (const ts of stamps) {
202
- const row = [new Date(ts).toISOString()]
203
- for (const id of cols) row.push(fmtNum(perNode.get(id)?.get(ts)))
204
- rowsCsv.push(row.map(csvCell).join(','))
205
- }
206
- const csv = rowsCsv.join('\r\n')
207
-
208
- // 5) 原生 fetch + AbortController(60s)multipart 上传(ctx.http 仅 JSON,传文件必须原生)
209
- const base = baseOf(ctx)
210
- if (!base) return { ok: false, error: 'diag.base_url 非法(仅允许 http/https 且 host 为 127.0.0.1/localhost)。' }
211
- const ac = new AbortController()
212
- const timer = setTimeout(() => ac.abort(), 60000)
213
- try {
214
- const fd = new FormData()
215
- fd.append('folder', UPLOAD_FOLDER)
216
- fd.append('files', new Blob([csv], { type: 'text/csv' }), safeName(line, toMs))
217
- const res = await fetch(`${base}/api/files/data/upload`, { method: 'POST', body: fd, signal: ac.signal, headers: authHeadersOf(ctx) })
218
- const body = await res.json().catch(() => null)
219
- if (!res.ok) throw new Error(`HTTP ${res.status} ${short(body)}`)
220
- if (!body || body.success !== true) throw new Error(`success!=true ${short(body)}`)
221
- const csvPath = String(body.data?.[0]?.path ?? '')
222
- if (!csvPath) throw new Error('上传响应缺少 data[0].path')
223
- return { ok: true, csvPath, rows: stamps.length, nodes: lineNodes.length, fromMs, toMs }
224
- }
225
- catch (err) {
226
- return { ok: false, error: `快照上传失败: ${err?.message ?? err}` }
227
- }
228
- finally {
229
- clearTimeout(timer)
230
- }
231
- }
232
-
233
- // ── 发起诊断(快照 → start → execute → kv 登记;异步,绝不等待) ────────────
234
-
235
- async function startDiagnosis(ctx, { line, fromMs, toMs, question, scene, source }) {
236
- const base = baseOf(ctx)
237
- if (!base) return { ok: false, error: 'diag.base_url 非法(仅允许 http/https 且 host 为 127.0.0.1/localhost)。' }
238
- if (startingLines.has(line)) return { ok: false, error: `产线 ${line} 已有诊断正在启动,请稍后再试。` }
239
- const existing = runningOfLine(ctx, line)
240
- if (existing) return { ok: false, error: `产线 ${line} 已有运行中的诊断(run_id=${existing.id}),请稍后用 diag_status 查询。` }
241
- startingLines.add(line)
242
- try {
243
- const snap = await snapshotCore(ctx, line, fromMs, toMs)
244
- if (!snap.ok) return { ok: false, error: snap.error }
245
- const q = question || `${line} 产线该时窗数据深度根因诊断`
246
- const sceneName = scene || `${line}_diag`
247
- const started = await jpost(ctx, `${base}/api/diagnosis/start`, {
248
- dataPath: snap.csvPath,
249
- sceneName,
250
- userQuestion: q,
251
- harness: harnessOf(ctx), // 缺省落 claude 引擎会因无 key 失败,必须显式(系统配置 plugins.diag-bridge.harness → kv → omp)
252
- enhancement: 'off',
253
- reportLanguage: 'zh',
254
- maxTurns: maxTurnsOf(ctx), // 修复循环会加转数;150 不够走完全管线
255
- timeoutMinutes: maxMinutesOf(ctx),
256
- })
257
- const runId = String(started.data?.runId ?? '')
258
- const name = String(started.data?.name ?? '')
259
- if (!runId) return { ok: false, error: `start 响应缺少 data.runId: ${short(started)}` }
260
- await jpost(ctx, `${base}/api/diagnosis/execute/${encodeURIComponent(runId)}`, {})
261
- ctx.kv.set(runKey(runId), {
262
- line, fromMs, toMs, question: q, scene: sceneName,
263
- status: 'running', createdAt: Date.now(), source, csvPath: snap.csvPath, name,
264
- })
265
- return { ok: true, runId, name, csvPath: snap.csvPath, rows: snap.rows, nodes: snap.nodes }
266
- }
267
- catch (err) {
268
- return { ok: false, error: `发起诊断失败: ${err?.message ?? err}` }
269
- }
270
- finally {
271
- startingLines.delete(line)
272
- }
273
- }
274
-
275
- // ── 入库管线(web 建文档 → backend 索引 → backend 经验;每步断言 success) ───
276
-
277
- async function ingestCompleted(ctx, runId, meta, st) {
278
- try {
279
- // kv 按插件命名空间隔离:rag-bridge 建的库不在本插件 kv 里,需自行从 catalog 幂等解析
280
- const webBase = String(ctx.config?.get?.('plugins.rag-bridge.web_url') || ctx.kv.get('kb.web_url') || 'http://127.0.0.1:6789').replace(/\/+$/, '')
281
- let kbId = String(ctx.kv.get('kb.id') || '').trim()
282
- if (!kbId) {
283
- const cat = await ctx.http.get(`${webBase}/api/kb/catalog`, { timeoutMs: 15000, headers: kbHeadersOf(ctx) })
284
- .then(r => r.json()).catch(() => null)
285
- const hit = (Array.isArray(cat?.knowledgeBases) ? cat.knowledgeBases : [])
286
- .find(kb => kb?.name === 'aw-industrial')
287
- if (hit?.kbId) {
288
- ctx.kv.set('kb.id', String(hit.kbId))
289
- kbId = String(hit.kbId)
290
- }
291
- }
292
- if (!kbId) {
293
- ctx.logger.warn(`诊断 ${runId} 已完成但找不到知识库 aw-industrial(rag-bridge 未初始化?),跳过入库`)
294
- return
295
- }
296
- const base = baseOf(ctx)
297
- const runName = String(st.name || meta.name || '')
298
- if (!base || !runName) throw new Error(`缺少诊断服务 base 或 run name(${runName})`)
299
-
300
- // 1) 报告全文。报告端点按「目录名」索引(时间戳_scene),而 status.name 是显示名
301
- // (scene_runId)——优先用 report_path 反推目录名,缺失时回退显示名。
302
- const rp = String(st.report_path || meta.reportPath || '')
303
- const repDir = rp.split(/[\\/]/).filter(Boolean).slice(-2, -1)[0] || runName
304
- const rep = await jget(ctx, `${base}/api/files/workspace/report/${encodeURIComponent(repDir)}`, 10000)
305
- const content = String(rep.data?.content ?? '')
306
- if (!content) throw new Error('报告内容为空')
307
-
308
- const apiBase = String(ctx.config?.get?.('plugins.rag-bridge.base_url') || ctx.kv.get('kb.base_url') || 'http://127.0.0.1:8770').replace(/\/+$/, '')
309
-
310
- // 2) web 建文档(响应 {success, document:{path,…}},path 供索引用)
311
- const doc = await jpost(ctx, `${webBase}/api/kb/documents/create`, { kbId, name: `${runName}.md`, content }, 15000, kbHeadersOf(ctx))
312
- const docPath = String(doc.document?.path ?? '')
313
- if (!docPath) throw new Error(`documents/create 响应缺少 document.path: ${short(doc)}`)
314
-
315
- // 3) backend 向量/图谱索引
316
- await jpost(ctx, `${apiBase}/api/v1/search/index-document`, {
317
- kb_id: kbId,
318
- doc_path: docPath,
319
- content,
320
- tags: [meta.line, '诊断', 'source:diag-bridge'].filter(Boolean),
321
- }, 30000, kbHeadersOf(ctx))
322
-
323
- // 4) backend 经验沉淀
324
- const q = String(meta.question || '')
325
- await jpost(ctx, `${apiBase}/api/v1/experience/${encodeURIComponent(kbId)}`, {
326
- title: `诊断:${q ? q.slice(0, 50) : runName}`,
327
- category: 'troubleshooting',
328
- problem: q,
329
- solution: content.slice(0, 800),
330
- result: 'success',
331
- tags: [meta.line, meta.scene].filter(Boolean),
332
- }, 30000, kbHeadersOf(ctx))
333
-
334
- ctx.kv.set(runKey(runId), { ...ctx.kv.get(runKey(runId)), stored: true, storedAt: Date.now() })
335
- ctx.logger.info(`诊断 ${runId}(产线 ${meta.line})报告已入库: ${docPath}`)
336
- }
337
- catch (err) {
338
- ctx.logger.warn(`诊断 ${runId} 入库失败(不重试): ${err?.message ?? err}`)
339
- }
340
- }
341
-
342
- // ── 轮询器:15s 扫描 kv 中 running 的 run → 更新状态 → 完成则入库 ──────────
343
-
344
- async function sweepOnce(ctx) {
345
- // running:常规轮询;completed 未入库:30 分钟内重试入库(入库管线曾失败时自愈)
346
- const running = kvRuns(ctx).filter(r =>
347
- r.meta?.status === 'running'
348
- || (r.meta?.status === 'completed' && r.meta?.stored !== true
349
- && Date.now() - (r.meta?.completedAt ?? 0) < 120 * MIN))
350
- if (!running.length) return
351
- const base = baseOf(ctx)
352
- if (!base) {
353
- ctx.logger.warn('diag.base_url 非法,本轮跳过诊断轮询')
354
- return
355
- }
356
- for (const { id, meta } of running) {
357
- try {
358
- const st = (await jget(ctx, `${base}/api/diagnosis/status/${encodeURIComponent(id)}`, 8000)).data ?? {}
359
- const status = st.engineStatus || st.status || meta.status
360
- if (status === 'completed') {
361
- ctx.kv.set(runKey(id), {
362
- ...meta,
363
- status: 'completed',
364
- completedAt: meta.completedAt ?? Date.now(), // 重试路径不续期,保证重试窗口会收敛
365
- name: st.name ?? meta.name,
366
- score: st.score ?? null,
367
- verdict: st.judge_verdict ?? null,
368
- reportPath: st.report_path ?? null,
369
- })
370
- // 入库为独立异步管线,失败仅 warn,不影响轮询(未入库的下轮重试)
371
- if (ctx.kv.get(runKey(id))?.stored !== true) {
372
- ingestCompleted(ctx, id, ctx.kv.get(runKey(id)), st).catch(() => {})
373
- }
374
- }
375
- else if (status === 'failed' || status === 'stopped') {
376
- ctx.kv.set(runKey(id), {
377
- ...meta,
378
- status,
379
- completedAt: Date.now(),
380
- name: st.name ?? meta.name,
381
- error: st.error_message ?? null,
382
- })
383
- ctx.logger.warn(`诊断 ${id}(产线 ${meta.line})结束: ${status}${st.error_message ? `(${st.error_message})` : ''}`)
384
- }
385
- else if (status !== meta.status) {
386
- ctx.kv.set(runKey(id), { ...meta, status, name: st.name ?? meta.name })
387
- }
388
- }
389
- catch (err) {
390
- ctx.logger.warn(`轮询诊断 ${id} 失败(下轮继续): ${err?.message ?? err}`)
391
- }
392
- }
393
- }
394
-
395
- // ── 插件入口 ─────────────────────────────────────────────────────────────
396
-
397
- export default {
398
- name: 'diag-bridge',
399
- version: '1.1.0',
400
- description: '深度诊断桥接:导出 DAQ 时序快照 CSV,发起 industrial-deep-diagnostic 深度诊断并跟踪状态,完成后报告自动入知识库。',
401
- auth: 'user',
402
- client: './client.mjs', // 前端面板(插件页注入;i18n 见 i18n.json)
403
- // 插件配置分组(声明式):连接 / 执行 / 自动化 三个独立分区
404
- configGroups: [
405
- { id: 'default', labelKey: 'plugin.diag-bridge.group.conn', label: '诊断服务连接', description: '后端地址与鉴权 Token', order: 420 },
406
- { id: 'run', labelKey: 'plugin.diag-bridge.group.run', label: '诊断执行参数', description: 'diag_run 的引擎与预算', order: 430 },
407
- { id: 'auto', labelKey: 'plugin.diag-bridge.group.auto', label: '自动诊断', description: '越限自动发起诊断(DAQ 采样事件驱动)', order: 440, collapsed: true },
408
- ],
409
- // 插件设置声明(key 编址 plugins.diag-bridge.<key>;labelKey 解析 i18n.json)
410
- settings: [
411
- { key: 'base_url', type: 'string', default: DEFAULT_BASE, group: 'default', labelKey: 'plugin.diag-bridge.settings.base_url', label: '诊断服务地址', description: 'industrial-deep-diagnostic 后端(http/https,host 限 127.0.0.1/localhost);保存即热生效' },
412
- { key: 'token', type: 'string', default: '', group: 'default', labelKey: 'plugin.diag-bridge.settings.token', label: '诊断 API Token', description: '诊断服务开启鉴权时(v4 起默认强制)的 API Token(Authorization: Bearer);空=匿名;保存即热生效' },
413
- { key: 'harness', type: 'string', default: '', group: 'run', labelKey: 'plugin.diag-bridge.settings.harness', label: '诊断引擎', description: 'diag_run 用的引擎 id(omp/claude/mock 等;空=取 kv/omp);无 Anthropic key 的机器请用 omp' },
414
- { key: 'max_turns', type: 'number', default: 0, min: 0, max: 2000, group: 'run', labelKey: 'plugin.diag-bridge.settings.max_turns', label: '诊断最大轮数', description: 'diagnosis/start 的 maxTurns(0=取 kv/内置默认 220)' },
415
- { key: 'max_minutes', type: 'number', default: 0, min: 0, max: 720, group: 'run', labelKey: 'plugin.diag-bridge.settings.max_minutes', label: '诊断超时(分钟)', description: 'diagnosis/start 的 timeoutMinutes(0=取 kv/内置默认 40)' },
416
- { key: 'auto_enabled', type: 'boolean', default: false, group: 'auto', labelKey: 'plugin.diag-bridge.settings.auto_enabled', label: '自动诊断', description: 'daq:sample 越限命中规则时自动发起深度诊断(每产线冷却 30 分钟)' },
417
- ],
418
- setup(ctx) {
419
- // 运行时追加分区与字段(ctx.config.defineGroup / defineField):
420
- // 字段 key 与声明式完全等价(plugins.diag-bridge.auto_rules),已保存的值不丢;
421
- // 这里演示「插件在 setup 里按条件调用平台 API 扩展自己的配置面」。
422
- ctx.config.defineGroup({
423
- id: 'rules',
424
- labelKey: 'plugin.diag-bridge.group.rules',
425
- label: '自动诊断规则',
426
- description: 'JSON 规则;命中即触发自动诊断(仅 auto_enabled=true 时生效)',
427
- order: 450,
428
- })
429
- ctx.config.defineField({
430
- key: 'auto_rules',
431
- type: 'string',
432
- default: '',
433
- group: 'rules',
434
- labelKey: 'plugin.diag-bridge.settings.auto_rules',
435
- label: '自动诊断规则',
436
- description: 'JSON: {"节点id":{"op":"gt|lt","value":数值}};命中即触发自动诊断(空=无规则)',
437
- })
438
- // 轮询器(15s)+ setup 重水化:恢复对 kv 中 running run 的跟踪(热重载安全)
439
- const sweep = () => sweepOnce(ctx).catch(err => ctx.logger.warn(`诊断轮询异常(继续): ${err?.message ?? err}`))
440
- ctx.timer.setInterval(sweep, 15000)
441
- sweep()
442
-
443
- // 自动诊断(默认关):daq:sample 命中规则 → 冷却 30min → 同管线发起(source:'auto')
444
- ctx.hooks.on('daq:sample', (s) => {
445
- try {
446
- if (!autoEnabledOf(ctx)) return
447
- if (!s || typeof s.value !== 'number' || !s.nodeId || !s.lineId) return
448
- const rule = parseAutoRules(autoRulesOf(ctx))[s.nodeId]
449
- if (!rule || typeof rule?.value !== 'number' || !['gt', 'lt'].includes(rule.op)) return
450
- const hit = rule.op === 'lt' ? s.value < rule.value : s.value > rule.value
451
- if (!hit) return
452
- const line = s.lineId
453
- const last = Number(ctx.kv.get(`cooldown:${line}`)) || 0
454
- if (Date.now() - last < AUTO_COOLDOWN_MS) return
455
- const now = Date.now()
456
- ctx.kv.set(`cooldown:${line}`, now) // 先占冷却,防同窗连发
457
- startDiagnosis(ctx, {
458
- line,
459
- fromMs: now - 60 * MIN,
460
- toMs: now,
461
- question: `${line} 产线该时窗数据深度根因诊断(自动触发:节点 ${s.nodeId} 采样值 ${s.value} ${rule.op === 'lt' ? '<' : '>'} ${rule.value})`,
462
- scene: `${line}_auto`,
463
- source: 'auto',
464
- }).then((r) => {
465
- if (r.ok) ctx.logger.warn(`自动诊断已发起:产线 ${line} runId=${r.runId}(节点 ${s.nodeId}=${s.value} 越限)`)
466
- else ctx.logger.warn(`自动诊断发起失败(产线 ${line}): ${r.error}`)
467
- }).catch(() => {})
468
- }
469
- catch { /* 钩子内绝不抛 */ }
470
- })
471
-
472
- // 工具 ×2
473
- ctx.omp.registerTool({
474
- name: 'diag_run',
475
- label: '深度诊断',
476
- description: '导出产线指定时窗的 DAQ 快照 CSV 并发起深度根因诊断(异步)。返回 runId 后需数分钟,请稍后用 diag_status 查询进度与结论。',
477
- parameters: {
478
- type: 'object',
479
- properties: {
480
- line: { type: 'string', description: '产线 id(必填)' },
481
- from_ms: { type: 'number', description: '窗口起点(epoch 毫秒;缺省 now-60min)' },
482
- to_ms: { type: 'number', description: '窗口终点(epoch 毫秒;缺省 now)' },
483
- question: { type: 'string', description: '诊断问题(缺省为该产线时窗数据深度根因诊断)' },
484
- scene: { type: 'string', description: '场景名(缺省 <line>_diag)' },
485
- },
486
- required: ['line'],
487
- },
488
- roles: ['lead', 'worker'],
489
- handler: async (args) => {
490
- try {
491
- const line = String(args.line ?? '').trim()
492
- if (!line) return { text: 'line 必填(产线 id)。', isError: true }
493
- const toMs = Number(args.to_ms) > 0 ? Number(args.to_ms) : Date.now()
494
- const fromMs = Number(args.from_ms) > 0 ? Number(args.from_ms) : toMs - 60 * MIN
495
- if (fromMs >= toMs) return { text: 'from_ms 必须小于 to_ms。', isError: true }
496
- const r = await startDiagnosis(ctx, {
497
- line, fromMs, toMs,
498
- question: args.question ? String(args.question) : '',
499
- scene: args.scene ? String(args.scene) : '',
500
- source: 'agent',
501
- })
502
- if (!r.ok) return { text: r.error, isError: true }
503
- return {
504
- text: `已发起深度诊断 runId=${r.runId}(产线 ${line},窗口 ${new Date(fromMs).toISOString()} ~ ${new Date(toMs).toISOString()},快照 ${r.rows} 行 × ${r.nodes} 节点 → ${r.csvPath})。诊断需数分钟,稍后用 diag_status 查询(run_id=${r.runId});完成后报告自动入知识库。`,
505
- }
506
- }
507
- catch (err) {
508
- return { text: `diag_run 异常: ${err?.message ?? err}`, isError: true }
509
- }
510
- },
511
- })
512
-
513
- ctx.omp.registerTool({
514
- name: 'diag_status',
515
- label: '诊断状态',
516
- description: '查询深度诊断进度:传 run_id 看单条实时状态(完成时含评分/结论/报告路径),不传看最近 5 条概要。',
517
- parameters: {
518
- type: 'object',
519
- properties: {
520
- run_id: { type: 'string', description: '诊断 runId(缺省列最近 5 条概要)' },
521
- },
522
- },
523
- roles: ['lead', 'worker'],
524
- handler: async (args) => {
525
- try {
526
- const id = String(args.run_id ?? '').trim()
527
- if (!id) {
528
- const runs = kvRuns(ctx)
529
- .sort((a, b) => (b.meta?.createdAt ?? 0) - (a.meta?.createdAt ?? 0))
530
- .slice(0, 5)
531
- if (!runs.length) return { text: '暂无诊断记录。用 diag_run 发起一次深度诊断。' }
532
- const lines = runs.map(({ id: rid, meta }) =>
533
- `- ${rid} | ${meta.line} | ${meta.status}${meta.stored ? '(已入库)' : ''} | ${meta.source ?? 'agent'} | ${meta.createdAt ? new Date(meta.createdAt).toISOString() : '?'} | ${meta.question ?? ''}`)
534
- return { text: `最近 ${runs.length} 条诊断:\n${lines.join('\n')}` }
535
- }
536
- const meta = ctx.kv.get(runKey(id))
537
- const base = baseOf(ctx)
538
- let st = null
539
- if (base) {
540
- try {
541
- st = (await jget(ctx, `${base}/api/diagnosis/status/${encodeURIComponent(id)}`, 8000)).data ?? null
542
- }
543
- catch { /* 3210 不可达时回退 kv 元数据 */ }
544
- }
545
- if (!st && !meta) return { text: `诊断 ${id} 不存在或诊断服务不可达。`, isError: true }
546
- const status = st?.engineStatus || st?.status || meta?.status || 'unknown'
547
- const parts = [
548
- `run_id=${id}`,
549
- `name=${st?.name ?? meta?.name ?? '?'}`,
550
- `产线=${meta?.line ?? st?.scene_name ?? '?'}`,
551
- `状态=${status}`,
552
- `来源=${meta?.source ?? '?'}`,
553
- ]
554
- if (meta?.fromMs && meta?.toMs) parts.push(`窗口=${new Date(meta.fromMs).toISOString()} ~ ${new Date(meta.toMs).toISOString()}`)
555
- if (meta?.question) parts.push(`问题=${meta.question}`)
556
- if (status === 'completed') {
557
- parts.push(`评分=${st?.score ?? meta?.score ?? '-'}`)
558
- parts.push(`结论=${st?.judge_verdict ?? meta?.verdict ?? '-'}`)
559
- parts.push(`report_path=${st?.report_path ?? meta?.reportPath ?? '-'}`)
560
- parts.push(`入库=${meta?.stored ? '是' : meta && st ? '否/进行中' : '?'}`)
561
- }
562
- if (status === 'failed' || status === 'stopped') parts.push(`错误=${st?.error_message ?? meta?.error ?? '-'}`)
563
- if (!st) parts.push('(诊断服务不可达,以上为本地记录)')
564
- return { text: parts.join('\n') }
565
- }
566
- catch (err) {
567
- return { text: `diag_status 异常: ${err?.message ?? err}`, isError: true }
568
- }
569
- },
570
- })
571
-
572
- // 路由 ×3(挂 /api/plugins/diag-bridge/*)
573
- ctx.route('GET', '/health', async () => {
574
- const runs = kvRuns(ctx)
575
- const byStatus = {}
576
- for (const { meta } of runs) byStatus[meta?.status ?? 'unknown'] = (byStatus[meta?.status ?? 'unknown'] ?? 0) + 1
577
- const base = baseOf(ctx)
578
- let remote
579
- let tokenState = { configured: Boolean(diagTokenOf(ctx)), accepted: null }
580
- if (base) {
581
- try {
582
- // 连通性:/api/health 是公开端点(无鉴权),只回答「服务在不在」。
583
- const res = await ctx.http.get(`${base}/api/health`, { timeoutMs: 5000 })
584
- const body = await res.json().catch(() => null)
585
- remote = res.ok && body
586
- ? { status: body.status ?? 'ok', activeRuns: body.checks?.activeRuns ?? null }
587
- : { status: `HTTP ${res.status}`, activeRuns: null }
588
- // Token 有效性:/api/health 不校验鉴权,必须另打一个受保护端点,
589
- // 否则 token 配错时健康面依旧 ok —— 配置错误完全不可见(实测踩过)。
590
- if (tokenState.configured) {
591
- try {
592
- const auth = await ctx.http.get(`${base}/api/files/workspace`, {
593
- timeoutMs: 5000,
594
- headers: authHeadersOf(ctx),
595
- })
596
- if (auth.status === 401 || auth.status === 403) {
597
- tokenState = { configured: true, accepted: false, hint: authHint(auth.status, 'Token 被上游拒绝') }
598
- remote = { ...remote, auth: `HTTP ${auth.status}(鉴权被拒)`, hint: tokenState.hint }
599
- }
600
- else if (auth.ok) {
601
- tokenState = { configured: true, accepted: true }
602
- remote = { ...remote, auth: 'ok' }
603
- }
604
- else {
605
- remote = { ...remote, auth: `HTTP ${auth.status}` }
606
- }
607
- }
608
- catch (err) {
609
- remote = { ...remote, auth: 'unreachable', authError: String(err?.message ?? err) }
610
- }
611
- }
612
- else {
613
- tokenState = { configured: false, accepted: null }
614
- }
615
- }
616
- catch (err) {
617
- remote = { status: 'unreachable', error: String(err?.message ?? err) }
618
- }
619
- }
620
- else {
621
- remote = { status: 'bad_base_url' }
622
- }
623
- return {
624
- plugin: ctx.name,
625
- base: base ?? String(ctx.kv.get('diag.base_url') || DEFAULT_BASE),
626
- harness: harnessOf(ctx),
627
- auth: diagTokenOf(ctx) ? 'bearer' : 'anonymous',
628
- /** Token 配置态 + 实测是否被上游接受(401/403 → false;网络不通 → null) */
629
- token: tokenState,
630
- remote,
631
- runs: { total: runs.length, byStatus },
632
- }
633
- })
634
-
635
- ctx.route('GET', '/runs', () => ({
636
- success: true,
637
- runs: kvRuns(ctx)
638
- .sort((a, b) => (b.meta?.createdAt ?? 0) - (a.meta?.createdAt ?? 0))
639
- .map(({ id, meta }) => ({ runId: id, ...meta })),
640
- }))
641
-
642
- ctx.route('POST', '/snapshot', async (event) => {
643
- try {
644
- const b = event?.awBody ?? {}
645
- const line = String(b.line ?? '').trim()
646
- if (!line) return { success: false, error: 'line 必填(产线 id)' }
647
- const toMs = Number(b.to_ms) > 0 ? Number(b.to_ms) : Date.now()
648
- const fromMs = Number(b.from_ms) > 0 ? Number(b.from_ms) : toMs - 60 * MIN
649
- if (fromMs >= toMs) return { success: false, error: 'from_ms 必须小于 to_ms' }
650
- const snap = await snapshotCore(ctx, line, fromMs, toMs)
651
- if (!snap.ok) return { success: false, error: snap.error }
652
- return { success: true, csvPath: snap.csvPath, rows: snap.rows, nodes: snap.nodes, fromMs: snap.fromMs, toMs: snap.toMs }
653
- }
654
- catch (err) {
655
- return { success: false, error: String(err?.message ?? err) }
656
- }
657
- })
658
-
659
- ctx.logger.info('diag-bridge 就绪:工具 diag_run/diag_status + 路由 health/runs/snapshot,轮询 15s 已启动')
660
- },
661
- }
1
+ /**
2
+ * diag-bridge —— industrial-deep-diagnostic 深度诊断服务桥接插件。
3
+ *
4
+ * 把诊断服务(http://127.0.0.1:3210)接入 AgentWorkShop:导出 DAQ 时序快照 CSV →
5
+ * 上传并发起深度诊断 → 轮询状态 → 完成后报告自动入知识库(文档/索引/经验三步)。
6
+ * 工具对 lead/worker 全体 agent 生效;3210 不可达时一律 isError 文本,绝不抛异常。
7
+ */
8
+ const MIN = 60 * 1000
9
+ const DEFAULT_BASE = 'http://127.0.0.1:3210'
10
+ const SNAPSHOT_BUCKET_MS = 5000 // 快照降采样桶宽
11
+ const SNAPSHOT_LIMIT = 10000 // 与 ctx.daq.query limit 一致(截断断言阈值)
12
+ const UPLOAD_FOLDER = 'aw-snapshots'
13
+ const AUTO_COOLDOWN_MS = 30 * MIN // 自动诊断同产线冷却
14
+
15
+ /** 每产线「启动中」互斥(进程内同步集合,堵住 kv 检查与远端落库之间的并发窗口) */
16
+ const startingLines = new Set()
17
+
18
+ // ── 基础工具 ──────────────────────────────────────────────────────────────
19
+
20
+ /** 诊断服务 base(kv diag.base_url;只允许 http/https 且 host 为 127.0.0.1/localhost) */
21
+ function baseOf(ctx) {
22
+ const raw = String(ctx.config?.get?.('plugins.diag-bridge.base_url') || ctx.kv.get('diag.base_url') || DEFAULT_BASE).trim()
23
+ try {
24
+ const u = new URL(raw)
25
+ const okProto = u.protocol === 'http:' || u.protocol === 'https:'
26
+ const okHost = u.hostname === '127.0.0.1' || u.hostname === 'localhost'
27
+ if (!okProto || !okHost) return null
28
+ return raw.replace(/\/+$/, '')
29
+ }
30
+ catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ /** 诊断服务鉴权 token(系统配置 plugins.diag-bridge.token 优先,kv diag.token 兜底;空=匿名)。
36
+ * 诊断服务 v4 起默认强制 Bearer(AUTH_ENABLED!==0),401 AUTH_REQUIRED 时先查这里。 */
37
+ function diagTokenOf(ctx) {
38
+ return String(ctx.config?.get?.('plugins.diag-bridge.token') || ctx.kv.get('diag.token') || '').trim()
39
+ }
40
+
41
+ function authHeadersOf(ctx) {
42
+ const t = diagTokenOf(ctx)
43
+ return t ? { authorization: `Bearer ${t}` } : {}
44
+ }
45
+
46
+ // ── 配置读取(系统配置优先,kv 兜底;前端全局设置→运行配置→插件组可改,保存即热生效) ──
47
+
48
+ const harnessOf = ctx => String(ctx.config?.get?.('plugins.diag-bridge.harness') || ctx.kv.get('diag.harness') || 'omp')
49
+ const maxTurnsOf = ctx => Number(ctx.config?.get?.('plugins.diag-bridge.max_turns')) || Number(ctx.kv.get('diag.max_turns')) || 220
50
+ const maxMinutesOf = ctx => Number(ctx.config?.get?.('plugins.diag-bridge.max_minutes')) || Number(ctx.kv.get('diag.max_minutes')) || 40
51
+ /** 自动诊断开关:系统配置布尔或 kv 'true'(kv 为历史字符串语义) */
52
+ const autoEnabledOf = ctx => ctx.config?.get?.('plugins.diag-bridge.auto_enabled') === true || ctx.kv.get('auto_diag_enabled') === 'true'
53
+ const autoRulesOf = ctx => String(ctx.config?.get?.('plugins.diag-bridge.auto_rules') || ctx.kv.get('auto_rules') || '')
54
+
55
+ const runKey = id => `run:${id}`
56
+
57
+ /** kv 中全部 run 记录 → [{ id, meta }] */
58
+ function kvRuns(ctx) {
59
+ const out = []
60
+ for (const [k, v] of Object.entries(ctx.kv.all())) {
61
+ if (k.startsWith('run:') && v && typeof v === 'object') out.push({ id: k.slice(4), meta: v })
62
+ }
63
+ return out
64
+ }
65
+
66
+ /** 该产线是否有运行中的诊断 */
67
+ function runningOfLine(ctx, line) {
68
+ return kvRuns(ctx).find(r => r.meta?.status === 'running' && r.meta?.line === line) ?? null
69
+ }
70
+
71
+ /** 短 JSON 摘要(错误信息用) */
72
+ function short(body) {
73
+ try {
74
+ return JSON.stringify(body).slice(0, 200)
75
+ }
76
+ catch {
77
+ return String(body)
78
+ }
79
+ }
80
+
81
+ /** 401 时给可操作的修复指引(生产最常见故障:会话 token 随诊断服务重启失效) */
82
+ function authHint(status, msg) {
83
+ if (status !== 401) return msg
84
+ return `${msg};修复:在 系统设置→运行配置→插件 更新「诊断 API Token」(推荐使用诊断服务侧的持久化 API Token,idd_ 前缀,服务重启不失效)`
85
+ }
86
+
87
+ /** JSON GET:断言 HTTP ok 且 body.success===true,返回 body */
88
+ async function jget(ctx, url, timeoutMs = 8000) {
89
+ const res = await ctx.http.get(url, { timeoutMs, headers: authHeadersOf(ctx) })
90
+ const body = await res.json().catch(() => null)
91
+ if (!res.ok) throw new Error(authHint(res.status, `HTTP ${res.status} ${short(body)}`))
92
+ if (!body || body.success !== true) throw new Error(`success!=true ${short(body)}`)
93
+ return body
94
+ }
95
+
96
+ /** JSON POST:断言 HTTP ok 且 body.success===true,返回 body(extraHeaders 供 KB 端点叠加鉴权) */
97
+ async function jpost(ctx, url, body, timeoutMs = 15000, extraHeaders = {}) {
98
+ const res = await ctx.http.post(url, body, { timeoutMs, headers: { ...authHeadersOf(ctx), ...extraHeaders } })
99
+ const out = await res.json().catch(() => null)
100
+ if (!res.ok) throw new Error(authHint(res.status, `HTTP ${res.status} ${short(out)}`))
101
+ if (!out || out.success !== true) throw new Error(`success!=true ${short(out)}`)
102
+ return out
103
+ }
104
+
105
+ /** 解析 auto_rules(kv 存 JSON 字符串 {nodeId:{op:'gt'|'lt',value}}) */
106
+ function parseAutoRules(raw) {
107
+ if (!raw) return {}
108
+ try {
109
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
110
+ return obj && typeof obj === 'object' ? obj : {}
111
+ }
112
+ catch {
113
+ return {}
114
+ }
115
+ }
116
+
117
+ /** CSV 单元格转义(含逗号/引号/换行时加引号) */
118
+ function csvCell(v) {
119
+ const s = String(v)
120
+ return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
121
+ }
122
+
123
+ /** 数值格式化(保留 6 位小数,去尾零;非有限值 → 空) */
124
+ function fmtNum(v) {
125
+ if (typeof v !== 'number' || !Number.isFinite(v)) return ''
126
+ return String(Math.round(v * 1e6) / 1e6)
127
+ }
128
+
129
+ /** 快照文件名(ASCII 安全,避免 multipart filename 编码问题) */
130
+ function safeName(line, toMs) {
131
+ const s = String(line).replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40) || 'line'
132
+ return `snap-${s}-${toMs}.csv`
133
+ }
134
+
135
+ // ── 快照导出器 ───────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * 导出产线时窗快照:daq 节点过滤 → 时序查询 → 时间戳 pivot → CSV → multipart 上传。
139
+ * 成功返回 { ok:true, csvPath, rows, nodes, fromMs, toMs };失败返回 { ok:false, error }。
140
+ */
141
+ async function snapshotCore(ctx, line, fromMs, toMs) {
142
+ if (!ctx.daq || typeof ctx.daq.nodes !== 'function' || typeof ctx.daq.query !== 'function') {
143
+ return { ok: false, error: '平台 ctx.daq 扩展未就绪(需宿主提供 daq.nodes / daq.query)。' }
144
+ }
145
+ // 1) 产线节点过滤(节点产线归属字段为 lineId,daq-node.repo snapshot 同构)
146
+ const all = await ctx.daq.nodes()
147
+ const lineNodes = (Array.isArray(all) ? all : []).filter(n => n && n.lineId === line)
148
+ if (!lineNodes.length) return { ok: false, error: `产线 ${line} 下无数采节点。` }
149
+
150
+ // 2) 时序查询(bucket 点含 {at,avg,min,max,cnt},raw 点含 {at,value});
151
+ // 打标查询空窗(LineRun 未开跑/空闲段样本无 line_id)时降级为按节点查询
152
+ let map = await ctx.daq.query({
153
+ lineId: line,
154
+ nodeIds: lineNodes.map(n => n.id),
155
+ fromMs,
156
+ toMs,
157
+ bucketMs: SNAPSHOT_BUCKET_MS,
158
+ limit: SNAPSHOT_LIMIT,
159
+ })
160
+ if (!(map instanceof Map) || map.size === 0) {
161
+ map = await ctx.daq.query({
162
+ nodeIds: lineNodes.map(n => n.id),
163
+ fromMs,
164
+ toMs,
165
+ bucketMs: SNAPSHOT_BUCKET_MS,
166
+ limit: SNAPSHOT_LIMIT,
167
+ })
168
+ }
169
+
170
+ // 3) pivot:收集全部时间戳排序去重;行=时间戳,列=每节点(bucket 取 avg,raw 取 value)
171
+ const tsSet = new Set()
172
+ const perNode = new Map() // nodeId → Map(at → value)
173
+ for (const [nodeId, points] of (map instanceof Map ? map : [])) {
174
+ if (!Array.isArray(points)) continue
175
+ const m = new Map()
176
+ for (const p of points) {
177
+ if (!p || typeof p.at !== 'number') continue
178
+ const v = typeof p.avg === 'number' ? p.avg : (typeof p.value === 'number' ? p.value : null)
179
+ if (v == null) continue
180
+ m.set(p.at, v)
181
+ tsSet.add(p.at)
182
+ }
183
+ if (m.size) perNode.set(nodeId, m)
184
+ }
185
+ const stamps = [...tsSet].sort((a, b) => a - b)
186
+ if (!stamps.length) return { ok: false, error: `产线 ${line} 在该时窗内无数采样本,请调整时间窗口。` }
187
+ if (stamps.length >= SNAPSHOT_LIMIT) {
188
+ return { ok: false, error: `唯一时间戳数(${stamps.length})≥ 上限 ${SNAPSHOT_LIMIT},数据可能被 limit 截断,请缩小时间窗口后重试。` }
189
+ }
190
+
191
+ // 4) 组 CSV(timestamp 用平台本地时区 ISO;缺值留空)
192
+ const cols = lineNodes.map(n => n.id)
193
+ const rowsCsv = ['timestamp,' + cols.map(csvCell).join(',')]
194
+ for (const ts of stamps) {
195
+ const row = [new Date(ts).toISOString()]
196
+ for (const id of cols) row.push(fmtNum(perNode.get(id)?.get(ts)))
197
+ rowsCsv.push(row.map(csvCell).join(','))
198
+ }
199
+ const csv = rowsCsv.join('\r\n')
200
+
201
+ // 5) 原生 fetch + AbortController(60s)multipart 上传(ctx.http 仅 JSON,传文件必须原生)
202
+ const base = baseOf(ctx)
203
+ if (!base) return { ok: false, error: 'diag.base_url 非法(仅允许 http/https 且 host 为 127.0.0.1/localhost)。' }
204
+ const ac = new AbortController()
205
+ const timer = setTimeout(() => ac.abort(), 60000)
206
+ try {
207
+ const fd = new FormData()
208
+ fd.append('folder', UPLOAD_FOLDER)
209
+ fd.append('files', new Blob([csv], { type: 'text/csv' }), safeName(line, toMs))
210
+ const res = await fetch(`${base}/api/files/data/upload`, { method: 'POST', body: fd, signal: ac.signal, headers: authHeadersOf(ctx) })
211
+ const body = await res.json().catch(() => null)
212
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${short(body)}`)
213
+ if (!body || body.success !== true) throw new Error(`success!=true ${short(body)}`)
214
+ const csvPath = String(body.data?.[0]?.path ?? '')
215
+ if (!csvPath) throw new Error('上传响应缺少 data[0].path')
216
+ return { ok: true, csvPath, rows: stamps.length, nodes: lineNodes.length, fromMs, toMs }
217
+ }
218
+ catch (err) {
219
+ return { ok: false, error: `快照上传失败: ${err?.message ?? err}` }
220
+ }
221
+ finally {
222
+ clearTimeout(timer)
223
+ }
224
+ }
225
+
226
+ // ── 发起诊断(快照 → start → execute → kv 登记;异步,绝不等待) ────────────
227
+
228
+ async function startDiagnosis(ctx, { line, fromMs, toMs, question, scene, source, dataPath }) {
229
+ const base = baseOf(ctx)
230
+ if (!base) return { ok: false, error: 'diag.base_url 非法(仅允许 http/https 且 host 为 127.0.0.1/localhost)。' }
231
+ if (startingLines.has(line)) return { ok: false, error: `产线 ${line} 已有诊断正在启动,请稍后再试。` }
232
+ const existing = runningOfLine(ctx, line)
233
+ if (existing) return { ok: false, error: `产线 ${line} 已有运行中的诊断(run_id=${existing.id}),请稍后用 diag_status 查询。` }
234
+ startingLines.add(line)
235
+ try {
236
+ let snap = { ok: true, csvPath: '', rows: 0, nodes: 0 }
237
+ if (!dataPath) {
238
+ snap = await snapshotCore(ctx, line, fromMs, toMs)
239
+ if (!snap.ok) return { ok: false, error: snap.error }
240
+ }
241
+ const q = question || `${line} 产线该时窗数据深度根因诊断`
242
+ const sceneName = scene || `${line}_diag`
243
+ // v2.1:统一走 IDD 任务管理面 /api/diagnosis/tasks——接收即返 task_id,
244
+ // 诊断在 IDD 后台线程执行(内部 start+execute),Channel 绝不等待。
245
+ const started = await jpost(ctx, `${base}/api/diagnosis/tasks`, {
246
+ dataPath: dataPath || snap.csvPath,
247
+ sceneName,
248
+ userQuestion: q,
249
+ harness: harnessOf(ctx), // 缺省落 claude 引擎会因无 key 失败,必须显式(系统配置 plugins.diag-bridge.harness → kv → omp)
250
+ enhancement: 'off',
251
+ reportLanguage: 'zh',
252
+ maxTurns: maxTurnsOf(ctx), // 修复循环会加转数;150 不够走完全管线
253
+ timeoutMinutes: maxMinutesOf(ctx),
254
+ })
255
+ const runId = String(started.data?.task_id ?? started.data?.runId ?? '')
256
+ const name = String(started.data?.name ?? '')
257
+ if (!runId) return { ok: false, error: `tasks 响应缺少 data.task_id: ${short(started)}` }
258
+ ctx.kv.set(runKey(runId), {
259
+ line, fromMs, toMs, question: q, scene: sceneName,
260
+ status: 'running', createdAt: Date.now(), source, csvPath: snap.csvPath, name,
261
+ })
262
+ return { ok: true, runId, name, csvPath: snap.csvPath, rows: snap.rows, nodes: snap.nodes }
263
+ }
264
+ catch (err) {
265
+ return { ok: false, error: `发起诊断失败: ${err?.message ?? err}` }
266
+ }
267
+ finally {
268
+ startingLines.delete(line)
269
+ }
270
+ }
271
+
272
+ // ── 轮询器:15s 扫描 kv 中 running 的 run → 更新状态 ──────────────────────
273
+ // v2.1:入库不再由本插件自动执行(web documents/create 旧两步管线已移除)。
274
+ // 完成后的协调契约:Channel 空闲时用 diag_status 查询 → 拿到 诊断总结+报告 md
275
+ // 路径 → 由 Agent 调用 rag-bridge 的 kb_agent(mode=async) 把报告入库知识库。
276
+
277
+ async function sweepOnce(ctx) {
278
+ const running = kvRuns(ctx).filter(r => r.meta?.status === 'running')
279
+ if (!running.length) return
280
+ const base = baseOf(ctx)
281
+ if (!base) {
282
+ ctx.logger.warn('diag.base_url 非法,本轮跳过诊断轮询')
283
+ return
284
+ }
285
+ for (const { id, meta } of running) {
286
+ try {
287
+ const st = (await jget(ctx, `${base}/api/diagnosis/status/${encodeURIComponent(id)}`, 8000)).data ?? {}
288
+ const status = st.engineStatus || st.status || meta.status
289
+ if (status === 'completed') {
290
+ ctx.kv.set(runKey(id), {
291
+ ...meta,
292
+ status: 'completed',
293
+ completedAt: meta.completedAt ?? Date.now(),
294
+ name: st.name ?? meta.name,
295
+ score: st.score ?? null,
296
+ verdict: st.judge_verdict ?? null,
297
+ reportPath: st.report_path ?? null,
298
+ })
299
+ ctx.logger.info(`诊断 ${id}(产线 ${meta.line})完成——待 Channel 空闲时 diag_status 查询,经 kb_agent 入库知识库`)
300
+ }
301
+ else if (status === 'failed' || status === 'stopped') {
302
+ ctx.kv.set(runKey(id), {
303
+ ...meta,
304
+ status,
305
+ completedAt: Date.now(),
306
+ name: st.name ?? meta.name,
307
+ error: st.error_message ?? null,
308
+ })
309
+ ctx.logger.warn(`诊断 ${id}(产线 ${meta.line})结束: ${status}${st.error_message ? `(${st.error_message})` : ''}`)
310
+ }
311
+ else if (status !== meta.status) {
312
+ ctx.kv.set(runKey(id), { ...meta, status, name: st.name ?? meta.name })
313
+ }
314
+ }
315
+ catch (err) {
316
+ ctx.logger.warn(`轮询诊断 ${id} 失败(下轮继续): ${err?.message ?? err}`)
317
+ }
318
+ }
319
+ }
320
+
321
+ // ── 插件入口 ─────────────────────────────────────────────────────────────
322
+
323
+ export default {
324
+ name: 'diag-bridge',
325
+ version: '2.1.0',
326
+ description: '深度诊断桥接 v2.1:异步任务式深度诊断(提交即返 task_id,Channel 免等待);完成后 diag_status 给出报告 md 路径,由 Agent 调 rag-bridge 的 kb_agent 入库知识库(插件协同)。',
327
+ auth: 'user',
328
+ client: './client.mjs', // 前端面板(插件页注入;i18n 见 i18n.json)
329
+ // 插件配置分组(声明式):连接 / 执行 / 自动化 三个独立分区
330
+ configGroups: [
331
+ { id: 'default', labelKey: 'plugin.diag-bridge.group.conn', label: '诊断服务连接', description: '后端地址与鉴权 Token', order: 420 },
332
+ { id: 'run', labelKey: 'plugin.diag-bridge.group.run', label: '诊断执行参数', description: 'diag_run 的引擎与预算', order: 430 },
333
+ { id: 'auto', labelKey: 'plugin.diag-bridge.group.auto', label: '自动诊断', description: '越限自动发起诊断(DAQ 采样事件驱动)', order: 440, collapsed: true },
334
+ ],
335
+ // 插件设置声明(key 编址 plugins.diag-bridge.<key>;labelKey 解析 i18n.json)
336
+ settings: [
337
+ { key: 'base_url', type: 'string', default: DEFAULT_BASE, group: 'default', labelKey: 'plugin.diag-bridge.settings.base_url', label: '诊断服务地址', description: 'industrial-deep-diagnostic 后端(http/https,host 限 127.0.0.1/localhost);保存即热生效' },
338
+ { key: 'token', type: 'string', default: '', group: 'default', labelKey: 'plugin.diag-bridge.settings.token', label: '诊断 API Token', description: '诊断服务开启鉴权时(v4 起默认强制)的 API Token(Authorization: Bearer);空=匿名;保存即热生效' },
339
+ { key: 'harness', type: 'string', default: '', group: 'run', labelKey: 'plugin.diag-bridge.settings.harness', label: '诊断引擎', description: 'diag_run 用的引擎 id(omp/claude/mock 等;空=取 kv/omp);无 Anthropic key 的机器请用 omp' },
340
+ { key: 'max_turns', type: 'number', default: 0, min: 0, max: 2000, group: 'run', labelKey: 'plugin.diag-bridge.settings.max_turns', label: '诊断最大轮数', description: 'diagnosis/start 的 maxTurns(0=取 kv/内置默认 220)' },
341
+ { key: 'max_minutes', type: 'number', default: 0, min: 0, max: 720, group: 'run', labelKey: 'plugin.diag-bridge.settings.max_minutes', label: '诊断超时(分钟)', description: 'diagnosis/start 的 timeoutMinutes(0=取 kv/内置默认 40)' },
342
+ { key: 'auto_enabled', type: 'boolean', default: false, group: 'auto', labelKey: 'plugin.diag-bridge.settings.auto_enabled', label: '自动诊断', description: 'daq:sample 越限命中规则时自动发起深度诊断(每产线冷却 30 分钟)' },
343
+ ],
344
+ setup(ctx) {
345
+ // 运行时追加分区与字段(ctx.config.defineGroup / defineField):
346
+ // 字段 key 与声明式完全等价(plugins.diag-bridge.auto_rules),已保存的值不丢;
347
+ // 这里演示「插件在 setup 里按条件调用平台 API 扩展自己的配置面」。
348
+ ctx.config.defineGroup({
349
+ id: 'rules',
350
+ labelKey: 'plugin.diag-bridge.group.rules',
351
+ label: '自动诊断规则',
352
+ description: 'JSON 规则;命中即触发自动诊断(仅 auto_enabled=true 时生效)',
353
+ order: 450,
354
+ })
355
+ ctx.config.defineField({
356
+ key: 'auto_rules',
357
+ type: 'string',
358
+ default: '',
359
+ group: 'rules',
360
+ labelKey: 'plugin.diag-bridge.settings.auto_rules',
361
+ label: '自动诊断规则',
362
+ description: 'JSON: {"节点id":{"op":"gt|lt","value":数值}};命中即触发自动诊断(空=无规则)',
363
+ })
364
+ // 轮询器(15s)+ setup 重水化:恢复对 kv 中 running run 的跟踪(热重载安全)
365
+ const sweep = () => sweepOnce(ctx).catch(err => ctx.logger.warn(`诊断轮询异常(继续): ${err?.message ?? err}`))
366
+ ctx.timer.setInterval(sweep, 15000)
367
+ sweep()
368
+
369
+ // 自动诊断(默认关):daq:sample 命中规则 → 冷却 30min → 同管线发起(source:'auto')
370
+ ctx.hooks.on('daq:sample', (s) => {
371
+ try {
372
+ if (!autoEnabledOf(ctx)) return
373
+ if (!s || typeof s.value !== 'number' || !s.nodeId || !s.lineId) return
374
+ const rule = parseAutoRules(autoRulesOf(ctx))[s.nodeId]
375
+ if (!rule || typeof rule?.value !== 'number' || !['gt', 'lt'].includes(rule.op)) return
376
+ const hit = rule.op === 'lt' ? s.value < rule.value : s.value > rule.value
377
+ if (!hit) return
378
+ const line = s.lineId
379
+ const last = Number(ctx.kv.get(`cooldown:${line}`)) || 0
380
+ if (Date.now() - last < AUTO_COOLDOWN_MS) return
381
+ const now = Date.now()
382
+ ctx.kv.set(`cooldown:${line}`, now) // 先占冷却,防同窗连发
383
+ startDiagnosis(ctx, {
384
+ line,
385
+ fromMs: now - 60 * MIN,
386
+ toMs: now,
387
+ question: `${line} 产线该时窗数据深度根因诊断(自动触发:节点 ${s.nodeId} 采样值 ${s.value} ${rule.op === 'lt' ? '<' : '>'} ${rule.value})`,
388
+ scene: `${line}_auto`,
389
+ source: 'auto',
390
+ }).then((r) => {
391
+ if (r.ok) ctx.logger.warn(`自动诊断已发起:产线 ${line} runId=${r.runId}(节点 ${s.nodeId}=${s.value} 越限)`)
392
+ else ctx.logger.warn(`自动诊断发起失败(产线 ${line}): ${r.error}`)
393
+ }).catch(() => {})
394
+ }
395
+ catch { /* 钩子内绝不抛 */ }
396
+ })
397
+
398
+ // 工具 ×2
399
+ ctx.omp.registerTool({
400
+ name: 'diag_run',
401
+ label: '深度诊断',
402
+ description: '导出产线指定时窗的 DAQ 快照 CSV 并发起深度根因诊断(异步任务,提交即返 task_id,无需等待)。Channel 可继续其他任务,空闲时用 diag_status 查询;完成后按其给出的指引调 kb_agent 入库知识库。',
403
+ parameters: {
404
+ type: 'object',
405
+ properties: {
406
+ line: { type: 'string', description: '产线 id(与 data_path 二选一)' },
407
+ data_path: { type: 'string', description: '可选:直接指定诊断数据 CSV 绝对路径(跳过 DAQ 快照导出,适合离线数据分析);给了 data_path 则 line 可填 any' },
408
+ from_ms: { type: 'number', description: '窗口起点(epoch 毫秒;缺省 now-60min)' },
409
+ to_ms: { type: 'number', description: '窗口终点(epoch 毫秒;缺省 now)' },
410
+ question: { type: 'string', description: '诊断问题(缺省为该产线时窗数据深度根因诊断)' },
411
+ scene: { type: 'string', description: '场景名(缺省 <line>_diag)' },
412
+ },
413
+ required: ['line'],
414
+ },
415
+ roles: ['lead', 'worker'],
416
+ handler: async (args) => {
417
+ try {
418
+ const line = String(args.line ?? '').trim()
419
+ const dataPathDirect = String(args.data_path ?? '').trim()
420
+ if (!line && !dataPathDirect) return { text: 'line 与 data_path 至少给一个(产线 id 或诊断数据 CSV 绝对路径)。', isError: true }
421
+ const toMs = Number(args.to_ms) > 0 ? Number(args.to_ms) : Date.now()
422
+ const fromMs = Number(args.from_ms) > 0 ? Number(args.from_ms) : toMs - 60 * MIN
423
+ if (fromMs >= toMs) return { text: 'from_ms 必须小于 to_ms。', isError: true }
424
+ const r = await startDiagnosis(ctx, {
425
+ line: line || 'offline-data',
426
+ fromMs, toMs,
427
+ question: args.question ? String(args.question) : '',
428
+ scene: args.scene ? String(args.scene) : '',
429
+ source: 'agent',
430
+ dataPath: dataPathDirect || '',
431
+ })
432
+ if (!r.ok) return { text: r.error, isError: true }
433
+ const via = dataPathDirect ? `离线数据 ${r.csvPath}` : `产线 ${line} 窗口 ${new Date(fromMs).toISOString()} ~ ${new Date(toMs).toISOString()} 快照 ${r.rows} 行 × ${r.nodes} 节点`
434
+ return {
435
+ text: `已发起深度诊断异步任务 task_id=${r.runId}(${via})。知识库 IDD 在后台执行,Channel 无需等待——可继续其他任务,空闲时用 diag_status(run_id=${r.runId}) 查询;完成后按 diag_status 给出的指引调用 kb_agent 把报告 md 入库知识库。`,
436
+ }
437
+ }
438
+ catch (err) {
439
+ return { text: `diag_run 异常: ${err?.message ?? err}`, isError: true }
440
+ }
441
+ },
442
+ })
443
+
444
+ ctx.omp.registerTool({
445
+ name: 'diag_status',
446
+ label: '诊断状态',
447
+ description: '查询深度诊断进度:传 run_id 看单条实时状态(完成时含评分/结论/报告路径),不传看最近 5 条概要。',
448
+ parameters: {
449
+ type: 'object',
450
+ properties: {
451
+ run_id: { type: 'string', description: '诊断 runId(缺省列最近 5 条概要)' },
452
+ },
453
+ },
454
+ roles: ['lead', 'worker'],
455
+ handler: async (args) => {
456
+ try {
457
+ const id = String(args.run_id ?? '').trim()
458
+ if (!id) {
459
+ const runs = kvRuns(ctx)
460
+ .sort((a, b) => (b.meta?.createdAt ?? 0) - (a.meta?.createdAt ?? 0))
461
+ .slice(0, 5)
462
+ if (!runs.length) return { text: '暂无诊断记录。用 diag_run 发起一次深度诊断。' }
463
+ const lines = runs.map(({ id: rid, meta }) =>
464
+ `- ${rid} | ${meta.line} | ${meta.status}${meta.stored ? '(已入库)' : ''} | ${meta.source ?? 'agent'} | ${meta.createdAt ? new Date(meta.createdAt).toISOString() : '?'} | ${meta.question ?? ''}`)
465
+ return { text: `最近 ${runs.length} 条诊断:\n${lines.join('\n')}` }
466
+ }
467
+ const meta = ctx.kv.get(runKey(id))
468
+ const base = baseOf(ctx)
469
+ let st = null
470
+ if (base) {
471
+ try {
472
+ // v2.1:走任务管理面 /tasks/:id——result.report_md_path 是绝对路径,
473
+ // 可直接交给 kb_agent 读取入库(旧 /status 的相对路径会让 kb_agent 找不到文件)
474
+ st = (await jget(ctx, `${base}/api/diagnosis/tasks/${encodeURIComponent(id)}`, 8000)).data ?? null
475
+ if (st && st.result) {
476
+ st = {
477
+ ...st,
478
+ engineStatus: st.status,
479
+ score: st.result.score ?? null,
480
+ judge_verdict: st.result.verdict ?? null,
481
+ report_path: st.result.report_md_path ?? st.report_path ?? null,
482
+ error_message: st.result.error ?? st.error_message ?? null,
483
+ }
484
+ }
485
+ }
486
+ catch { /* 3210 不可达时回退 kv 元数据 */ }
487
+ }
488
+ if (!st && !meta) return { text: `诊断 ${id} 不存在或诊断服务不可达。`, isError: true }
489
+ const status = st?.engineStatus || st?.status || meta?.status || 'unknown'
490
+ const parts = [
491
+ `run_id=${id}`,
492
+ `name=${st?.name ?? meta?.name ?? '?'}`,
493
+ `产线=${meta?.line ?? st?.scene_name ?? '?'}`,
494
+ `状态=${status}`,
495
+ `来源=${meta?.source ?? '?'}`,
496
+ ]
497
+ if (meta?.fromMs && meta?.toMs) parts.push(`窗口=${new Date(meta.fromMs).toISOString()} ~ ${new Date(meta.toMs).toISOString()}`)
498
+ if (meta?.question) parts.push(`问题=${meta.question}`)
499
+ if (status === 'completed') {
500
+ const reportPath = st?.report_path ?? meta?.reportPath ?? '-'
501
+ parts.push(`评分=${st?.score ?? meta?.score ?? '-'}`)
502
+ parts.push(`结论=${st?.judge_verdict ?? meta?.verdict ?? '-'}`)
503
+ parts.push(`report_md_path=${reportPath}`)
504
+ parts.push('')
505
+ parts.push('✅ 诊断完成,报告已就绪。入库知识库请调用 rag-bridge 的 kb_agent 工具:')
506
+ parts.push(` mode=async, prompt=「请把这份深度诊断报告入库到 aw-industrial 知识库:读取文件 ${reportPath} 的全文内容并入库(写盘+索引),完成后报告 doc_path 与分块数。标签:诊断、深度诊断。」`)
507
+ parts.push('提交后频道无需等待,空闲时用 kb_agent_status(task_id) 查询入库结果。')
508
+ }
509
+ if (status === 'failed' || status === 'stopped') parts.push(`错误=${st?.error_message ?? meta?.error ?? '-'}`)
510
+ if (!st) parts.push('(诊断服务不可达,以上为本地记录)')
511
+ return { text: parts.join('\n') }
512
+ }
513
+ catch (err) {
514
+ return { text: `diag_status 异常: ${err?.message ?? err}`, isError: true }
515
+ }
516
+ },
517
+ })
518
+
519
+ // 路由 ×3(挂 /api/plugins/diag-bridge/*)
520
+ ctx.route('GET', '/health', async () => {
521
+ const runs = kvRuns(ctx)
522
+ const byStatus = {}
523
+ for (const { meta } of runs) byStatus[meta?.status ?? 'unknown'] = (byStatus[meta?.status ?? 'unknown'] ?? 0) + 1
524
+ const base = baseOf(ctx)
525
+ let remote
526
+ let tokenState = { configured: Boolean(diagTokenOf(ctx)), accepted: null }
527
+ if (base) {
528
+ try {
529
+ // 连通性:/api/health 是公开端点(无鉴权),只回答「服务在不在」。
530
+ const res = await ctx.http.get(`${base}/api/health`, { timeoutMs: 5000 })
531
+ const body = await res.json().catch(() => null)
532
+ remote = res.ok && body
533
+ ? { status: body.status ?? 'ok', activeRuns: body.checks?.activeRuns ?? null }
534
+ : { status: `HTTP ${res.status}`, activeRuns: null }
535
+ // Token 有效性:/api/health 不校验鉴权,必须另打一个受保护端点,
536
+ // 否则 token 配错时健康面依旧 ok —— 配置错误完全不可见(实测踩过)。
537
+ if (tokenState.configured) {
538
+ try {
539
+ const auth = await ctx.http.get(`${base}/api/files/workspace`, {
540
+ timeoutMs: 5000,
541
+ headers: authHeadersOf(ctx),
542
+ })
543
+ if (auth.status === 401 || auth.status === 403) {
544
+ tokenState = { configured: true, accepted: false, hint: authHint(auth.status, 'Token 被上游拒绝') }
545
+ remote = { ...remote, auth: `HTTP ${auth.status}(鉴权被拒)`, hint: tokenState.hint }
546
+ }
547
+ else if (auth.ok) {
548
+ tokenState = { configured: true, accepted: true }
549
+ remote = { ...remote, auth: 'ok' }
550
+ }
551
+ else {
552
+ remote = { ...remote, auth: `HTTP ${auth.status}` }
553
+ }
554
+ }
555
+ catch (err) {
556
+ remote = { ...remote, auth: 'unreachable', authError: String(err?.message ?? err) }
557
+ }
558
+ }
559
+ else {
560
+ tokenState = { configured: false, accepted: null }
561
+ }
562
+ }
563
+ catch (err) {
564
+ remote = { status: 'unreachable', error: String(err?.message ?? err) }
565
+ }
566
+ }
567
+ else {
568
+ remote = { status: 'bad_base_url' }
569
+ }
570
+ return {
571
+ plugin: ctx.name,
572
+ base: base ?? String(ctx.kv.get('diag.base_url') || DEFAULT_BASE),
573
+ harness: harnessOf(ctx),
574
+ auth: diagTokenOf(ctx) ? 'bearer' : 'anonymous',
575
+ /** Token 配置态 + 实测是否被上游接受(401/403 → false;网络不通 → null) */
576
+ token: tokenState,
577
+ remote,
578
+ runs: { total: runs.length, byStatus },
579
+ }
580
+ })
581
+
582
+ ctx.route('GET', '/runs', () => ({
583
+ success: true,
584
+ runs: kvRuns(ctx)
585
+ .sort((a, b) => (b.meta?.createdAt ?? 0) - (a.meta?.createdAt ?? 0))
586
+ .map(({ id, meta }) => ({ runId: id, ...meta })),
587
+ }))
588
+
589
+ ctx.route('POST', '/snapshot', async (event) => {
590
+ try {
591
+ const b = event?.awBody ?? {}
592
+ const line = String(b.line ?? '').trim()
593
+ if (!line) return { success: false, error: 'line 必填(产线 id)' }
594
+ const toMs = Number(b.to_ms) > 0 ? Number(b.to_ms) : Date.now()
595
+ const fromMs = Number(b.from_ms) > 0 ? Number(b.from_ms) : toMs - 60 * MIN
596
+ if (fromMs >= toMs) return { success: false, error: 'from_ms 必须小于 to_ms' }
597
+ const snap = await snapshotCore(ctx, line, fromMs, toMs)
598
+ if (!snap.ok) return { success: false, error: snap.error }
599
+ return { success: true, csvPath: snap.csvPath, rows: snap.rows, nodes: snap.nodes, fromMs: snap.fromMs, toMs: snap.toMs }
600
+ }
601
+ catch (err) {
602
+ return { success: false, error: String(err?.message ?? err) }
603
+ }
604
+ })
605
+
606
+ ctx.logger.info('diag-bridge 就绪:工具 diag_run/diag_status + 路由 health/runs/snapshot,轮询 15s 已启动')
607
+ },
608
+ }