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,804 +1,812 @@
1
- <script setup lang="ts">
2
- /**
3
- * 左栏 Channel 会话列表(Zcode session 栏):workspace 内挂载的 channel,
4
- * 实时状态徽标(忙碌成员数/活跃任务数),点击聚焦;挂载/移出/设置操作。
5
- * v10 模板化:
6
- * - 「从模板挂载」:选择 Channel 模板 → 实例化 + 挂载一步完成(替代"挂载已有 Channel");
7
- * - 「设置」:热修改当前 Channel 实例的场景描述与工作目录(成员运行时自动回收重装配);
8
- * - 「保存为模板」:把当前 Channel 实例捕获为可复用模板(场景/目录/团队快照)。
9
- */
10
- import { message } from 'ant-design-vue'
11
- import { useWorkspacesStore } from '@/app/stores/workshop/workspaces'
12
- import { useEntitiesStore } from '@/app/stores/workshop/entities'
13
- import { useWorkshopApi, type ChannelDto, type ChannelTemplateDto } from '@/app/composables/workshop/useWorkshopApi'
14
-
15
- const { t } = useI18n()
16
-
17
- const props = defineProps<{ wsId: string }>()
18
- const wsStore = useWorkspacesStore()
19
- const entities = useEntitiesStore()
20
- const api = useWorkshopApi()
21
-
22
- const workspace = computed(() => wsStore.workspaces.find(w => w.id === props.wsId))
23
-
24
- const channels = ref<ChannelDto[]>([])
25
- const channelTemplates = ref<ChannelTemplateDto[]>([])
26
- const refreshChannels = async (): Promise<void> => {
27
- // SSR 守卫:axios 相对 baseURL 仅客户端有效(服务端拉取会 Invalid URL)
28
- if (typeof window === 'undefined') return
29
- const [ch, tpl] = await Promise.all([
30
- api.listChannels(),
31
- api.listChannelTemplates().catch(() => null),
32
- ])
33
- channels.value = (ch as unknown as { data?: ChannelDto[] })?.data ?? []
34
- channelTemplates.value = (tpl as unknown as { data?: ChannelTemplateDto[] } | null)?.data ?? []
35
- }
36
- void refreshChannels()
37
-
38
- const mountedChannels = computed(() =>
39
- (workspace.value?.channelIds ?? [])
40
- .map(id => ({ id, meta: channels.value.find(c => c.id === id), entity: entities.channels[id] }))
41
- .map(({ id, meta, entity }) => ({
42
- id,
43
- name: entity?.name ?? meta?.name ?? id.slice(0, 8),
44
- /** 实体基线(WS 快照)是否已到达:未到时计数不可信,展示"同步中"而非误导性的 0 */
45
- synced: entity !== undefined,
46
- busy: entities.busyCount(id),
47
- agents: entities.agents[id]?.length ?? 0,
48
- activeTasks: (entities.tasks[id] ?? []).filter(t => !['COMPLETED', 'CANCELED', 'FAILED'].includes(t.state)).length,
49
- workspace: meta?.workspace ?? '',
50
- })),
51
- )
52
-
53
- const select = (channelId: string): void => {
54
- wsStore.setActiveChannel(props.wsId, channelId)
55
- }
56
-
57
- /** 点击工作目录行:复制完整路径到剪贴板(有真实作用;title 提示全路径) */
58
- const copyWorkspace = async (path: string): Promise<void> => {
59
- try {
60
- await navigator.clipboard.writeText(path)
61
- message.success(t('channelSessionList.kud74vg040', { p0: path }))
62
- }
63
- catch {
64
- message.error(t('channelSessionList.krij3gg026'))
65
- }
66
- }
67
- /** 路径显示 basename(E:\codes\AgentWorkShop → AgentWorkShop),全路径在 title/复制 */
68
- const baseName = (path: string): string => path.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? path
69
- /** 默认目录(data/workspaces/<uuid>)的 basename 是 36 位裸 UUID,对用户零信息量 → 不展示该行 */
70
- const UUID_RE = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i
71
- const displayWorkspace = (path: string): string => {
72
- const base = baseName(path)
73
- return UUID_RE.test(base) ? '' : base
74
- }
75
-
76
- // ===== 新建 Channel 并挂载(空团队) =====
77
- const mountModal = ref(false)
78
- const mountForm = reactive({ name: '', description: '', workspace: '', scenarioPrompt: '' })
79
- const mountSubmitting = ref(false)
80
- /** FileSelector 弹窗(选择服务器目录作为 workspace) */
81
- const fileSelectorOpen = ref(false)
82
- const createAndMount = async (): Promise<void> => {
83
- if (!mountForm.name.trim()) {
84
- message.warning('Channel 名称必填')
85
- return
86
- }
87
- mountSubmitting.value = true
88
- try {
89
- const res = await api.createChannel({
90
- name: mountForm.name.trim(),
91
- description: mountForm.description || undefined,
92
- scenarioPrompt: mountForm.scenarioPrompt.trim() || undefined,
93
- workspace: mountForm.workspace.trim() || undefined,
94
- })
95
- const created = (res as unknown as { data?: { channelId?: string } })?.data
96
- const channelId = created?.channelId
97
- if (!channelId) throw new Error(t('channelSessionList.k1bg6shc027'))
98
- await wsStore.mountChannel(props.wsId, channelId)
99
- mountModal.value = false
100
- mountForm.name = ''
101
- mountForm.description = ''
102
- mountForm.workspace = ''
103
- mountForm.scenarioPrompt = ''
104
- void refreshChannels()
105
- message.success(t('channelSessionList.kn89jvs028'))
106
- }
107
- catch (e) {
108
- message.error(t('channelSessionList.k1x2th9e041', { p0: apiErrorMessage(e) }))
109
- }
110
- finally {
111
- mountSubmitting.value = false
112
- }
113
- }
114
-
115
- // ===== 从 Channel 模板挂载(实例化 + mount 一步) =====
116
- const templateMountId = ref<string | undefined>()
117
- const templateMounting = ref(false)
118
- const mountFromTemplate = async (): Promise<void> => {
119
- if (!templateMountId.value) return
120
- templateMounting.value = true
121
- try {
122
- const res = await api.mountChannelTemplate(props.wsId, templateMountId.value)
123
- const data = (res as unknown as { data?: { agentCount?: number } })?.data
124
- message.success(t('channelSessionList.kai50qc042', { p0: data?.agentCount ?? 0 }))
125
- templateMountId.value = undefined
126
- void refreshChannels()
127
- }
128
- catch (e) {
129
- const err = e as { data?: { message?: string }, message?: string }
130
- message.error(apiErrorMessage(err, t('channelSessionList.k97xi7u029')))
131
- }
132
- finally {
133
- templateMounting.value = false
134
- }
135
- }
136
-
137
- const unmount = (channelId: string): void => {
138
- wsStore.unmountChannel(props.wsId, channelId)
139
- .catch((e: { data?: { message?: string }, message?: string }) => { message.error(apiErrorMessage(e, t('channelSessionList.k1hgfvdc030'))) })
140
- }
141
-
142
- // ===== Channel 实例设置(场景/工作目录热更新)+ 保存为模板 =====
143
- const settingsOpen = ref(false)
144
- const settingsChannelId = ref<string>('')
145
- const settingsForm = reactive({ name: '', scenarioPrompt: '', workspace: '' })
146
-
147
- // ===== Channel 级默认 LLM(harness → provider/model/effort;空 = 用引擎默认) =====
148
- interface CatalogProvider {
149
- id: string
150
- models: Array<{ id: string, efforts: string[], defaultEffort?: string }>
151
- }
152
- const llmForm = reactive({ harness: 'omp', provider: '', model: '', effort: '', enabled: false })
153
- const llmProviders = ref<CatalogProvider[]>([])
154
- const llmEffortMode = ref<'levels' | 'freetext' | 'unsupported'>('levels')
155
- const llmLoading = ref(false)
156
- const llmProviderOptions = computed(() => llmProviders.value.map(p => ({ value: p.id, label: p.id })))
157
- const llmModelOptions = computed(() =>
158
- (llmProviders.value.find(p => p.id === llmForm.provider)?.models ?? []).map(m => ({ value: m.id, label: m.id })),
159
- )
160
- const llmEffortOptions = computed(() => {
161
- const m = llmProviders.value.find(p => p.id === llmForm.provider)?.models.find(x => x.id === llmForm.model)
162
- return (m?.efforts ?? []).map(e => ({ value: e, label: e + (m?.defaultEffort === e ? '(默认)' : '') }))
163
- })
164
- const loadLlmCatalog = async (harness: string): Promise<void> => {
165
- llmLoading.value = true
166
- llmProviders.value = []
167
- try {
168
- const res = await api.listHarnessProviders(harness)
169
- const cat = (res as unknown as { data?: { catalog?: { providers?: CatalogProvider[], effortMode?: 'levels' | 'freetext' | 'unsupported' } } })?.data?.catalog
170
- llmProviders.value = cat?.providers ?? []
171
- llmEffortMode.value = cat?.effortMode ?? 'unsupported'
172
- }
173
- catch { /* 目录拉取失败保留空表 */ }
174
- finally { llmLoading.value = false }
175
- }
176
- const onLlmHarnessChange = async (): Promise<void> => {
177
- llmForm.provider = ''
178
- llmForm.model = ''
179
- llmForm.effort = ''
180
- await loadLlmCatalog(llmForm.harness)
181
- }
182
- const onLlmProviderChange = (): void => {
183
- llmForm.model = ''
184
- llmForm.effort = ''
185
- }
186
- const onLlmModelChange = (): void => {
187
- llmForm.effort = ''
188
- }
189
- const settingsSaving = ref(false)
190
- const fileSelectorOpen2 = ref(false)
191
- /** channel 设置弹窗预填的默认 LLM(llmJson 反序列化形状;与服务端 channelLlmSchema 对齐) */
192
- interface ChannelLlmDefaults { provider?: string, model?: string, effort?: string }
193
- const openSettings = (channelId: string): void => {
194
- const meta = channels.value.find(c => c.id === channelId)
195
- settingsChannelId.value = channelId
196
- settingsForm.name = meta?.name ?? ''
197
- settingsForm.scenarioPrompt = meta?.scenarioPrompt ?? ''
198
- settingsForm.workspace = meta?.workspace ?? ''
199
- // 预填 channel 默认 LLM(llmJson 由列表接口透传)
200
- let saved: ChannelLlmDefaults | null = null
201
- try {
202
- const metaAny = meta as unknown as { llmJson?: string } | undefined
203
- saved = metaAny?.llmJson ? JSON.parse(metaAny.llmJson) as ChannelLlmDefaults : null
204
- }
205
- catch { saved = null }
206
- llmForm.enabled = !!saved?.model
207
- llmForm.harness = 'omp'
208
- llmForm.provider = saved?.provider ?? ''
209
- llmForm.model = saved?.model ?? ''
210
- llmForm.effort = saved?.effort ?? ''
211
- settingsOpen.value = true
212
- if (llmForm.enabled) void loadLlmCatalog(llmForm.harness)
213
- void loadChannelPlugins(channelId)
214
- }
215
-
216
- // ===== Channel 级插件开关(设置弹窗内嵌;切换即 PUT,该 channel 在跑 Agent 工具清单热刷新) =====
217
- interface ChannelPluginRow { name: string, description?: string, builtin?: boolean, enabled: boolean }
218
- const pluginRows = ref<ChannelPluginRow[]>([])
219
- const pluginSource = ref<'explicit' | 'default'>('default')
220
- const pluginSaving = ref<string | null>(null)
221
- const loadChannelPlugins = async (channelId: string): Promise<void> => {
222
- try {
223
- const res = await api.listChannelPlugins(channelId)
224
- const data = res?.data ?? {}
225
- pluginRows.value = data.plugins ?? []
226
- pluginSource.value = data.source === 'explicit' ? 'explicit' : 'default'
227
- }
228
- catch { /* 插件视图不可得(未登录/网络)时留空,不阻塞设置弹窗 */ }
229
- }
230
- const toggleChannelPlugin = async (row: ChannelPluginRow, next: boolean): Promise<void> => {
231
- if (pluginSaving.value) return
232
- pluginSaving.value = row.name
233
- try {
234
- // 全量提交当前开关视图(仅翻转目标行),与 teams 页插件开关同语义
235
- const payload = pluginRows.value.map(r => ({ name: r.name, enabled: r.name === row.name ? next : r.enabled }))
236
- const res = await api.putChannelPlugins(settingsChannelId.value, { plugins: payload })
237
- const data = res?.data ?? {}
238
- pluginRows.value = data.plugins ?? payload.map(p => ({ ...p }))
239
- pluginSource.value = data.source === 'explicit' ? 'explicit' : 'default'
240
- message.success(t('channelSessionList.k1plugon044'))
241
- }
242
- catch (e) {
243
- message.error(apiErrorMessage(e))
244
- }
245
- finally {
246
- pluginSaving.value = null
247
- }
248
- }
249
- const saveSettings = async (): Promise<void> => {
250
- settingsSaving.value = true
251
- try {
252
- await api.patchChannel(settingsChannelId.value, {
253
- scenarioPrompt: settingsForm.scenarioPrompt,
254
- workspace: settingsForm.workspace.trim() || undefined,
255
- llm: llmForm.enabled && llmForm.model
256
- ? { ...(llmForm.provider ? { provider: llmForm.provider } : {}), model: llmForm.model, ...(llmForm.effort ? { effort: llmForm.effort } : {}) }
257
- : null,
258
- })
259
- message.success(t('channelSessionList.kvsxlu6031'))
260
- settingsOpen.value = false
261
- void refreshChannels()
262
- }
263
- catch (e) {
264
- message.error(apiErrorMessage(e))
265
- }
266
- finally {
267
- settingsSaving.value = false
268
- }
269
- }
270
-
271
- // 保存为模板(捕获当前 channel 的场景/目录/团队)
272
- const saveTplOpen = ref(false)
273
- const saveTplForm = reactive({ name: '', description: '', visibility: 'private' as 'private' | 'public' })
274
- const saveTplSubmitting = ref(false)
275
- const openSaveTemplate = (): void => {
276
- saveTplForm.name = t('channelSessionList.k2hfym5043', { p0: settingsForm.name || 'channel' })
277
- saveTplForm.description = ''
278
- saveTplForm.visibility = 'private'
279
- saveTplOpen.value = true
280
- }
281
- const saveAsTemplate = async (): Promise<void> => {
282
- if (!saveTplForm.name.trim()) {
283
- message.warning(t('channelSessionList.k1i0ji0y032'))
284
- return
285
- }
286
- saveTplSubmitting.value = true
287
- try {
288
- await api.captureChannelTemplate({
289
- channelId: settingsChannelId.value,
290
- name: saveTplForm.name.trim(),
291
- description: saveTplForm.description || undefined,
292
- visibility: saveTplForm.visibility,
293
- })
294
- message.success(t('channelSessionList.kjpguxw033'))
295
- saveTplOpen.value = false
296
- void refreshChannels()
297
- }
298
- catch (e) {
299
- message.error(apiErrorMessage(e))
300
- }
301
- finally {
302
- saveTplSubmitting.value = false
303
- }
304
- }
305
- </script>
306
-
307
- <template>
308
- <div class="channel-list">
309
- <div class="list-head">
310
- <span class="title">Channels</span>
311
- <span class="head-actions">
312
- <button
313
- class="head-op im"
314
- type="button"
315
- title="Channel 模板中心"
316
- @click="navigateTo('/workshop/channel-templates')"
317
- >
318
- <span class="i-tabler-layout-grid-add im-pop" />
319
- </button>
320
- <button
321
- class="head-op im"
322
- type="button"
323
- title="新建 Channel"
324
- @click="mountModal = true"
325
- >
326
- <span class="i-tabler-plus im-pop" />
327
- </button>
328
- </span>
329
- </div>
330
-
331
- <!-- 频道列表主体:数据源(REST channels + WS entities)均为客户端态,
332
- SSR 首帧为空 → 硬刷新水合时结构与客户端首渲染不一致,节点会错配到
333
- 相邻 antd Select 的 DOM 上(行内 .row1 布局丢失、按钮错位)。
334
- ClientOnly 隔离:SSR 输出空壳,客户端一次性渲染,无水合配对。 -->
335
- <ClientOnly>
336
- <div
337
- v-if="mountedChannels.length === 0"
338
- class="empty"
339
- >
340
- {{ $t('channelSessionList.kkv789s014') }}
341
- </div>
342
-
343
- <div
344
- v-for="ch in mountedChannels"
345
- :key="ch.id"
346
- class="channel-item"
347
- :class="{ active: workspace?.activeChannelId === ch.id }"
348
- @click="select(ch.id)"
349
- >
350
- <div class="row1">
351
- <span
352
- class="dot"
353
- :class="{ live: ch.activeTasks > 0 }"
354
- />
355
- <span class="ch-name">{{ ch.name }}</span>
356
- <button
357
- class="op im"
358
- type="button"
359
- :title="$t('channelSessionList.kne35ug001')"
360
- :aria-label="$t('channelSessionList.kjmldih035', { p0: ch.name })"
361
- @click.stop="openSettings(ch.id)"
362
- >
363
- <span class="i-tabler-settings2 im-pop" />
364
- </button>
365
- <button
366
- class="op im"
367
- type="button"
368
- title="移出 workspace"
369
- :aria-label="$t('channelSessionList.k1yurooi036', { p0: ch.name })"
370
- @click.stop="unmount(ch.id)"
371
- >
372
- <span class="i-tabler-x im-pop" />
373
- </button>
374
- </div>
375
- <div class="row2">
376
- <!-- 快照未到达:计数不可信,显示同步占位(不呈现误导性的"0 成员") -->
377
- <span
378
- v-if="!ch.synced"
379
- class="meta syncing"
380
- >{{ $t('channelSessionList.k1bst7s9015') }}</span>
381
- <span
382
- v-else
383
- class="meta"
384
- >{{ ch.agents }} {{ $t('channelSessionList.k1ggoa45025') }} {{ ch.busy }} / {{ $t('channelSessionList.k3wcox034') }} {{ ch.activeTasks }}</span>
385
- </div>
386
- <button
387
- v-if="displayWorkspace(ch.workspace)"
388
- class="row2 ws im"
389
- type="button"
390
- :title="$t('channelSessionList.kfe4pzq037', { p0: ch.workspace })"
391
- @click.stop="copyWorkspace(ch.workspace)"
392
- >
393
- <span class="i-tabler-folder" /> {{ displayWorkspace(ch.workspace) }}
394
- </button>
395
- </div>
396
- </ClientOnly>
397
-
398
- <div class="mount-template">
399
- <a-select
400
- v-model:value="templateMountId"
401
- size="small"
402
- :placeholder="'从 Channel 模板挂载'"
403
- class="select"
404
- :loading="templateMounting"
405
- :options="channelTemplates.map(t => ({
406
- value: t.id,
407
- label: $t('channelSessionList.k1oes209038', { p0: t.isBuiltin ? '内置 · ' : t.visibility === 'public' ? '公开 · ' : '', p1: t.name, p2: (t.lead ? 1 : 0) + t.members.length }),
408
- }))"
409
- @change="mountFromTemplate"
410
- />
411
- <div class="tpl-hint">
412
- {{ $t('channelSessionList.k18ppu31016') }}
413
- </div>
414
- </div>
415
-
416
- <a-modal
417
- v-model:open="mountModal"
418
- :title="$t('channelSessionList.k1qjyl6l002')"
419
- :confirm-loading="mountSubmitting"
420
- ok-text="创建"
421
- cancel-text="取消"
422
- @ok="createAndMount"
423
- >
424
- <a-form layout="vertical">
425
- <a-form-item label="Channel 名称">
426
- <a-input v-model:value="mountForm.name" />
427
- </a-form-item>
428
- <a-form-item :label="$t('channelSessionList.k40gkk003')">
429
- <a-input v-model:value="mountForm.description" />
430
- </a-form-item>
431
- <a-form-item :label="$t('channelSessionList.k1wj4f38004')">
432
- <a-textarea
433
- v-model:value="mountForm.scenarioPrompt"
434
- :rows="4"
435
- :placeholder="$t('channelSessionList.k1098zie005')"
436
- />
437
- <template #extra>
438
- <span class="ws-hint">{{ $t('channelSessionList.k1u3zsr5017') }}</span>
439
- </template>
440
- </a-form-item>
441
- <a-form-item :label="$t('channelSessionList.kmgv4h5006')">
442
- <a-input-group compact>
443
- <a-input
444
- v-model:value="mountForm.workspace"
445
- style="width: 70%"
446
- placeholder="留空 = data/workspaces/<channelId>"
447
- allow-clear
448
- @press-enter="createAndMount"
449
- />
450
- <a-button
451
- style="width: 30%"
452
- @click="fileSelectorOpen = true"
453
- >
454
- {{ $t('channelSessionList.k3pz0ma018') }}
455
- </a-button>
456
- </a-input-group>
457
- <template #extra>
458
- <span class="ws-hint">{{ $t('channelSessionList.kucq30k019') }}</span>
459
- </template>
460
- </a-form-item>
461
- <a-form-item>
462
- <span class="ws-hint">{{ $t('channelSessionList.k1ful761020') }}</span>
463
- </a-form-item>
464
- </a-form>
465
- </a-modal>
466
-
467
- <!-- Channel 实例设置:场景/工作目录热更新 + 保存为模板 -->
468
- <a-modal
469
- v-model:open="settingsOpen"
470
- :title="$t('channelSessionList.k1pmemvt039', { p0: settingsForm.name })"
471
- :confirm-loading="settingsSaving"
472
- ok-text="保存"
473
- cancel-text="取消"
474
- @ok="saveSettings"
475
- >
476
- <a-form layout="vertical">
477
- <a-form-item :label="$t('channelSessionList.k1i8q46x007')">
478
- <a-textarea
479
- v-model:value="settingsForm.scenarioPrompt"
480
- :rows="4"
481
- :placeholder="$t('channelSessionList.ku3ugac008')"
482
- />
483
- </a-form-item>
484
- <a-form-item :label="$t('channelSessionList.k67dzev009')">
485
- <a-input-group compact>
486
- <a-input
487
- v-model:value="settingsForm.workspace"
488
- style="width: 70%"
489
- placeholder="data/workspaces/<channelId>"
490
- allow-clear
491
- />
492
- <a-button
493
- style="width: 30%"
494
- @click="fileSelectorOpen2 = true"
495
- >
496
- {{ $t('channelSessionList.k3pz0ma018') }}
497
- </a-button>
498
- </a-input-group>
499
- </a-form-item>
500
- <a-form-item label="默认模型(可选;留空用各 Harness 默认)">
501
- <a-space>
502
- <a-switch
503
- v-model:checked="llmForm.enabled"
504
- size="small"
505
- @change="(v: any) => { if (v) void loadLlmCatalog(llmForm.harness) }"
506
- />
507
- <a-select
508
- v-model:value="llmForm.harness"
509
- style="width: 130px"
510
- :options="[{ value: 'omp', label: 'omp' }, { value: 'codex', label: 'codex' }, { value: 'opencode', label: 'opencode' }, { value: 'dsh', label: 'dsh' }]"
511
- :disabled="!llmForm.enabled"
512
- @change="onLlmHarnessChange"
513
- />
514
- <a-select
515
- v-model:value="llmForm.provider"
516
- style="width: 170px"
517
- :placeholder="llmLoading ? '目录加载中…' : 'provider'"
518
- :options="llmProviderOptions"
519
- :disabled="!llmForm.enabled || llmLoading"
520
- show-search
521
- @change="onLlmProviderChange"
522
- />
523
- <a-select
524
- v-model:value="llmForm.model"
525
- style="width: 210px"
526
- placeholder="model"
527
- :options="llmModelOptions"
528
- :disabled="!llmForm.enabled || !llmForm.provider"
529
- show-search
530
- @change="onLlmModelChange"
531
- />
532
- <a-select
533
- v-if="llmEffortMode === 'levels'"
534
- v-model:value="llmForm.effort"
535
- style="width: 130px"
536
- placeholder="effort(可选)"
537
- :options="llmEffortOptions"
538
- :disabled="!llmForm.enabled || !llmForm.model"
539
- allow-clear
540
- />
541
- <a-input
542
- v-else-if="llmEffortMode === 'freetext'"
543
- v-model:value="llmForm.effort"
544
- style="width: 130px"
545
- placeholder="variant(可选)"
546
- :disabled="!llmForm.enabled || !llmForm.model"
547
- allow-clear
548
- />
549
- </a-space>
550
- </a-form-item>
551
- <a-form-item :label="t('channelSessionList.k1pluglbl045')">
552
- <div
553
- v-if="pluginRows.length"
554
- class="ch-plugin-list"
555
- >
556
- <div
557
- v-for="p in pluginRows"
558
- :key="p.name"
559
- class="ch-plugin-row"
560
- >
561
- <div class="ch-plugin-main">
562
- <span class="aw-mono">{{ p.name }}</span>
563
- <span
564
- v-if="p.builtin"
565
- class="ws-hint"
566
- >内置</span>
567
- <span
568
- v-if="p.description"
569
- class="ws-hint ch-plugin-desc"
570
- >{{ p.description }}</span>
571
- </div>
572
- <a-switch
573
- :checked="p.enabled"
574
- size="small"
575
- :loading="pluginSaving === p.name"
576
- @change="(v: any) => toggleChannelPlugin(p, !!v)"
577
- />
578
- </div>
579
- <span
580
- v-if="pluginSource !== 'explicit'"
581
- class="ws-hint"
582
- >{{ t('channelSessionList.k1plugsrc046') }}</span>
583
- </div>
584
- <span
585
- v-else
586
- class="ws-hint"
587
- >—</span>
588
- </a-form-item>
589
- <a-form-item>
590
- <a-button
591
- size="small"
592
- @click="openSaveTemplate"
593
- >
594
- <span class="i-tabler-template" />
595
- {{ $t('channelSessionList.k6kpql010') }}
596
- </a-button>
597
- <span class="ws-hint">{{ $t('channelSessionList.k1x6kznr021') }}</span>
598
- </a-form-item>
599
- </a-form>
600
- </a-modal>
601
-
602
- <!-- 保存为模板 -->
603
- <a-modal
604
- v-model:open="saveTplOpen"
605
- :title="$t('channelSessionList.k6kpql010')"
606
- :confirm-loading="saveTplSubmitting"
607
- ok-text="保存模板"
608
- cancel-text="取消"
609
- @ok="saveAsTemplate"
610
- >
611
- <a-form layout="vertical">
612
- <a-form-item :label="$t('channelSessionList.k1f55q76011')">
613
- <a-input v-model:value="saveTplForm.name" />
614
- </a-form-item>
615
- <a-form-item :label="$t('channelSessionList.k40gkk003')">
616
- <a-input v-model:value="saveTplForm.description" />
617
- </a-form-item>
618
- <a-form-item :label="$t('channelSessionList.k3lrqn0012')">
619
- <a-radio-group v-model:value="saveTplForm.visibility">
620
- <a-radio value="private">
621
- {{ $t('channelSessionList.k1otvrfv022') }}
622
- </a-radio>
623
- <a-radio value="public">
624
- {{ $t('channelSessionList.k1cc399s023') }}
625
- </a-radio>
626
- </a-radio-group>
627
- </a-form-item>
628
- <a-form-item>
629
- <span class="ws-hint">{{ $t('channelSessionList.kl9btf7024') }}</span>
630
- </a-form-item>
631
- </a-form>
632
- </a-modal>
633
-
634
- <!-- FileSelector:服务器目录选择 -> 回填工作目录 -->
635
- <workshop-file-selector-modal
636
- v-model:open="fileSelectorOpen"
637
- :title="$t('channelSessionList.kbdp56k013')"
638
- :initial-path="mountForm.workspace || undefined"
639
- @select="(p) => { mountForm.workspace = p }"
640
- />
641
- <workshop-file-selector-modal
642
- v-model:open="fileSelectorOpen2"
643
- :title="$t('channelSessionList.kbdp56k013')"
644
- :initial-path="settingsForm.workspace || undefined"
645
- @select="(p) => { settingsForm.workspace = p }"
646
- />
647
- </div>
648
- </template>
649
-
650
- <style scoped>
651
- .ch-plugin-list {
652
- display: flex;
653
- flex-direction: column;
654
- gap: 6px;
655
- width: 100%;
656
- }
657
- .ch-plugin-row {
658
- display: flex;
659
- align-items: center;
660
- justify-content: space-between;
661
- gap: 12px;
662
- padding: 4px 8px;
663
- border: 1px solid var(--border, rgba(128, 128, 128, 0.25));
664
- border-radius: 6px;
665
- }
666
- .ch-plugin-main {
667
- display: flex;
668
- align-items: baseline;
669
- gap: 8px;
670
- min-width: 0;
671
- }
672
- .ch-plugin-desc {
673
- overflow: hidden;
674
- text-overflow: ellipsis;
675
- white-space: nowrap;
676
- }
677
- .channel-list {
678
- display: flex;
679
- flex-direction: column;
680
- min-height: 100%;
681
- padding: 12px 10px 8px;
682
- }
683
- .list-head {
684
- display: flex;
685
- align-items: center;
686
- justify-content: space-between;
687
- padding: 0 8px 8px;
688
- }
689
- .title {
690
- font-family: var(--font-display);
691
- font-size: 16px;
692
- letter-spacing: -0.01em;
693
- color: var(--ink);
694
- }
695
- .head-actions { display: inline-flex; gap: 2px; }
696
- .head-op {
697
- display: inline-flex;
698
- align-items: center;
699
- justify-content: center;
700
- width: 24px;
701
- height: 24px;
702
- font-size: 14px;
703
- color: var(--ink-faint);
704
- cursor: pointer;
705
- background: transparent;
706
- border: 0;
707
- border-radius: var(--radius-chip);
708
- transition: background var(--transition-fast), color var(--transition-fast);
709
- }
710
- .head-op:hover { color: var(--ink); background: var(--paper-deep); }
711
- .empty {
712
- padding: 14px 10px;
713
- font-size: 12px;
714
- color: var(--ink-faint);
715
- }
716
- .channel-item {
717
- padding: 7px 9px;
718
- margin: 1px 0;
719
- cursor: pointer;
720
- border-radius: var(--radius-panel-sm);
721
- transition: background var(--transition-fast);
722
- }
723
- .channel-item:hover { background: var(--paper-deep); }
724
- .channel-item.active { background: var(--paper-deep); box-shadow: inset 2px 0 0 var(--accent); }
725
- .row1 {
726
- display: flex;
727
- gap: 7px;
728
- align-items: center;
729
- font-size: 13px;
730
- color: var(--ink-soft);
731
- }
732
- .channel-item.active .row1 { color: var(--ink); font-weight: 500; }
733
- .dot {
734
- flex: 0 0 auto;
735
- width: 7px;
736
- height: 7px;
737
- background: var(--tone-neutral-dot);
738
- opacity: 0.55;
739
- border-radius: 50%;
740
- }
741
- .dot.live {
742
- background: var(--tone-success-dot);
743
- opacity: 1;
744
- }
745
- .ch-name {
746
- flex: 1 1 auto;
747
- overflow: hidden;
748
- text-overflow: ellipsis;
749
- white-space: nowrap;
750
- }
751
- .op {
752
- display: inline-flex;
753
- flex: 0 0 auto;
754
- align-items: center;
755
- justify-content: center;
756
- width: 20px;
757
- height: 20px;
758
- padding: 0;
759
- font-size: 13px;
760
- color: var(--ink-faint);
761
- cursor: pointer;
762
- background: transparent;
763
- border: 0;
764
- border-radius: var(--radius-chip);
765
- /* 常显(低透明度):不再 hover-only——行级操作入口不应时隐时现 */
766
- opacity: 0.55;
767
- transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
768
- }
769
- .channel-item:hover .op,
770
- .channel-item.active .op { opacity: 0.9; }
771
- .op:hover { opacity: 1; color: var(--ink); background: var(--paper-deep); }
772
- .op:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
773
- .row2 { padding-left: 14px; font-size: 11px; color: var(--ink-faint); }
774
- .meta.syncing { font-style: italic; opacity: 0.6; }
775
- .row2.ws {
776
- display: flex;
777
- gap: 4px;
778
- align-items: center;
779
- overflow: hidden;
780
- max-width: 100%;
781
- padding: 0 2px 0 14px;
782
- font-family: var(--font-mono);
783
- font-size: 11px;
784
- text-overflow: ellipsis;
785
- white-space: nowrap;
786
- color: var(--ink-faint);
787
- text-align: left;
788
- cursor: copy; /* 点击复制完整路径(title 提示) */
789
- background: transparent;
790
- border: 0;
791
- border-radius: var(--radius-chip);
792
- transition: color var(--transition-fast), background var(--transition-fast);
793
- }
794
- .row2.ws:hover { color: var(--ink-soft); background: color-mix(in srgb, currentColor 6%, transparent); }
795
- .row2.ws:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
796
- .mount-template { padding: 12px 6px 6px; border-top: 1px solid var(--line); margin-top: 10px; }
797
- .select { width: 100%; }
798
- .tpl-hint {
799
- padding: 5px 2px 0;
800
- font-size: 10.5px;
801
- color: var(--ink-faint);
802
- }
803
- .ws-hint { font-size: 11px; color: var(--ink-faint); }
804
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * 左栏 Channel 会话列表(Zcode session 栏):workspace 内挂载的 channel,
4
+ * 实时状态徽标(忙碌成员数/活跃任务数),点击聚焦;挂载/移出/设置操作。
5
+ * v10 模板化:
6
+ * - 「从模板挂载」:选择 Channel 模板 → 实例化 + 挂载一步完成(替代"挂载已有 Channel");
7
+ * - 「设置」:热修改当前 Channel 实例的场景描述与工作目录(成员运行时自动回收重装配);
8
+ * - 「保存为模板」:把当前 Channel 实例捕获为可复用模板(场景/目录/团队快照)。
9
+ */
10
+ import { message } from 'ant-design-vue'
11
+ import { useWorkspacesStore } from '@/app/stores/workshop/workspaces'
12
+ import { useEntitiesStore } from '@/app/stores/workshop/entities'
13
+ import { useWorkshopApi, type ChannelDto, type ChannelTemplateDto } from '@/app/composables/workshop/useWorkshopApi'
14
+
15
+ const { t } = useI18n()
16
+
17
+ const props = defineProps<{ wsId: string }>()
18
+ const wsStore = useWorkspacesStore()
19
+ const entities = useEntitiesStore()
20
+ const api = useWorkshopApi()
21
+
22
+ const workspace = computed(() => wsStore.workspaces.find(w => w.id === props.wsId))
23
+
24
+ const channels = ref<ChannelDto[]>([])
25
+ const channelTemplates = ref<ChannelTemplateDto[]>([])
26
+ const refreshChannels = async (): Promise<void> => {
27
+ // SSR 守卫:axios 相对 baseURL 仅客户端有效(服务端拉取会 Invalid URL)
28
+ if (typeof window === 'undefined') return
29
+ const [ch, tpl] = await Promise.all([
30
+ api.listChannels(),
31
+ api.listChannelTemplates().catch(() => null),
32
+ ])
33
+ channels.value = (ch as unknown as { data?: ChannelDto[] })?.data ?? []
34
+ channelTemplates.value = (tpl as unknown as { data?: ChannelTemplateDto[] } | null)?.data ?? []
35
+ }
36
+ void refreshChannels()
37
+
38
+ const mountedChannels = computed(() =>
39
+ (workspace.value?.channelIds ?? [])
40
+ .map(id => ({ id, meta: channels.value.find(c => c.id === id), entity: entities.channels[id] }))
41
+ .map(({ id, meta, entity }) => ({
42
+ id,
43
+ name: entity?.name ?? meta?.name ?? id.slice(0, 8),
44
+ /** 实体基线(WS 快照)是否已到达:未到时计数不可信,展示"同步中"而非误导性的 0 */
45
+ synced: entity !== undefined,
46
+ busy: entities.busyCount(id),
47
+ agents: entities.agents[id]?.length ?? 0,
48
+ activeTasks: (entities.tasks[id] ?? []).filter(t => !['COMPLETED', 'CANCELED', 'FAILED'].includes(t.state)).length,
49
+ workspace: meta?.workspace ?? '',
50
+ })),
51
+ )
52
+
53
+ const select = (channelId: string): void => {
54
+ wsStore.setActiveChannel(props.wsId, channelId)
55
+ }
56
+
57
+ /** 点击工作目录行:复制完整路径到剪贴板(有真实作用;title 提示全路径) */
58
+ const copyWorkspace = async (path: string): Promise<void> => {
59
+ try {
60
+ await navigator.clipboard.writeText(path)
61
+ message.success(t('channelSessionList.kud74vg040', { p0: path }))
62
+ }
63
+ catch {
64
+ message.error(t('channelSessionList.krij3gg026'))
65
+ }
66
+ }
67
+ /** 路径显示 basename(E:\codes\AgentWorkShop → AgentWorkShop),全路径在 title/复制 */
68
+ const baseName = (path: string): string => path.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? path
69
+ /** 默认目录(data/workspaces/<uuid>)的 basename 是 36 位裸 UUID,对用户零信息量 → 不展示该行 */
70
+ const UUID_RE = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i
71
+ const displayWorkspace = (path: string): string => {
72
+ const base = baseName(path)
73
+ return UUID_RE.test(base) ? '' : base
74
+ }
75
+
76
+ // ===== 新建 Channel 并挂载(空团队) =====
77
+ const mountModal = ref(false)
78
+ const mountForm = reactive({ name: '', description: '', workspace: '', scenarioPrompt: '' })
79
+ const mountSubmitting = ref(false)
80
+ /** FileSelector 弹窗(选择服务器目录作为 workspace) */
81
+ const fileSelectorOpen = ref(false)
82
+ const createAndMount = async (): Promise<void> => {
83
+ if (!mountForm.name.trim()) {
84
+ message.warning(t('channelSessionList.nameRequired'))
85
+ return
86
+ }
87
+ mountSubmitting.value = true
88
+ try {
89
+ const res = await api.createChannel({
90
+ name: mountForm.name.trim(),
91
+ description: mountForm.description || undefined,
92
+ scenarioPrompt: mountForm.scenarioPrompt.trim() || undefined,
93
+ workspace: mountForm.workspace.trim() || undefined,
94
+ })
95
+ const created = (res as unknown as { data?: { channelId?: string } })?.data
96
+ const channelId = created?.channelId
97
+ if (!channelId) throw new Error(t('channelSessionList.k1bg6shc027'))
98
+ await wsStore.mountChannel(props.wsId, channelId)
99
+ mountModal.value = false
100
+ mountForm.name = ''
101
+ mountForm.description = ''
102
+ mountForm.workspace = ''
103
+ mountForm.scenarioPrompt = ''
104
+ void refreshChannels()
105
+ message.success(t('channelSessionList.kn89jvs028'))
106
+ }
107
+ catch (e) {
108
+ message.error(t('channelSessionList.k1x2th9e041', { p0: apiErrorMessage(e) }))
109
+ }
110
+ finally {
111
+ mountSubmitting.value = false
112
+ }
113
+ }
114
+
115
+ // ===== 从 Channel 模板挂载(实例化 + mount 一步) =====
116
+ const templateMountId = ref<string | undefined>()
117
+ const templateMounting = ref(false)
118
+ const mountFromTemplate = async (): Promise<void> => {
119
+ if (!templateMountId.value) return
120
+ templateMounting.value = true
121
+ try {
122
+ const res = await api.mountChannelTemplate(props.wsId, templateMountId.value)
123
+ const data = (res as unknown as { data?: { agentCount?: number, channelId?: string } })?.data
124
+ message.success(t('channelSessionList.kai50qc042', { p0: data?.agentCount ?? 0 }))
125
+ templateMountId.value = undefined
126
+ void refreshChannels()
127
+ // 一步挂载端点只返回 channelId,不会走 wsStore.mountChannel 的本地清单维护 ——
128
+ // 必须在这里补上 workspace.channelIds 本地更新并选中新频道,
129
+ // 否则列表要等下一次全量 load 才出现新频道(实测踩坑:刚挂载的频道"消失")
130
+ if (data?.channelId) {
131
+ const ws = wsStore.workspaces.find(w => w.id === props.wsId)
132
+ if (ws && !ws.channelIds.includes(data.channelId)) ws.channelIds.push(data.channelId)
133
+ wsStore.setActiveChannel(props.wsId, data.channelId)
134
+ }
135
+ }
136
+ catch (e) {
137
+ const err = e as { data?: { message?: string }, message?: string }
138
+ message.error(apiErrorMessage(err, t('channelSessionList.k97xi7u029')))
139
+ }
140
+ finally {
141
+ templateMounting.value = false
142
+ }
143
+ }
144
+
145
+ const unmount = (channelId: string): void => {
146
+ wsStore.unmountChannel(props.wsId, channelId)
147
+ .catch((e: { data?: { message?: string }, message?: string }) => { message.error(apiErrorMessage(e, t('channelSessionList.k1hgfvdc030'))) })
148
+ }
149
+
150
+ // ===== Channel 实例设置(场景/工作目录热更新)+ 保存为模板 =====
151
+ const settingsOpen = ref(false)
152
+ const settingsChannelId = ref<string>('')
153
+ const settingsForm = reactive({ name: '', scenarioPrompt: '', workspace: '' })
154
+
155
+ // ===== Channel 级默认 LLM(harness → provider/model/effort;空 = 用引擎默认) =====
156
+ interface CatalogProvider {
157
+ id: string
158
+ models: Array<{ id: string, efforts: string[], defaultEffort?: string }>
159
+ }
160
+ const llmForm = reactive({ harness: 'omp', provider: '', model: '', effort: '', enabled: false })
161
+ const llmProviders = ref<CatalogProvider[]>([])
162
+ const llmEffortMode = ref<'levels' | 'freetext' | 'unsupported'>('levels')
163
+ const llmLoading = ref(false)
164
+ const llmProviderOptions = computed(() => llmProviders.value.map(p => ({ value: p.id, label: p.id })))
165
+ const llmModelOptions = computed(() =>
166
+ (llmProviders.value.find(p => p.id === llmForm.provider)?.models ?? []).map(m => ({ value: m.id, label: m.id })),
167
+ )
168
+ const llmEffortOptions = computed(() => {
169
+ const m = llmProviders.value.find(p => p.id === llmForm.provider)?.models.find(x => x.id === llmForm.model)
170
+ return (m?.efforts ?? []).map(e => ({ value: e, label: e + (m?.defaultEffort === e ? t('channelSessionList.defaultSuffix') : '') }))
171
+ })
172
+ const loadLlmCatalog = async (harness: string): Promise<void> => {
173
+ llmLoading.value = true
174
+ llmProviders.value = []
175
+ try {
176
+ const res = await api.listHarnessProviders(harness)
177
+ const cat = (res as unknown as { data?: { catalog?: { providers?: CatalogProvider[], effortMode?: 'levels' | 'freetext' | 'unsupported' } } })?.data?.catalog
178
+ llmProviders.value = cat?.providers ?? []
179
+ llmEffortMode.value = cat?.effortMode ?? 'unsupported'
180
+ }
181
+ catch { /* 目录拉取失败保留空表 */ }
182
+ finally { llmLoading.value = false }
183
+ }
184
+ const onLlmHarnessChange = async (): Promise<void> => {
185
+ llmForm.provider = ''
186
+ llmForm.model = ''
187
+ llmForm.effort = ''
188
+ await loadLlmCatalog(llmForm.harness)
189
+ }
190
+ const onLlmProviderChange = (): void => {
191
+ llmForm.model = ''
192
+ llmForm.effort = ''
193
+ }
194
+ const onLlmModelChange = (): void => {
195
+ llmForm.effort = ''
196
+ }
197
+ const settingsSaving = ref(false)
198
+ const fileSelectorOpen2 = ref(false)
199
+ /** channel 设置弹窗预填的默认 LLM(llmJson 反序列化形状;与服务端 channelLlmSchema 对齐) */
200
+ interface ChannelLlmDefaults { provider?: string, model?: string, effort?: string }
201
+ const openSettings = (channelId: string): void => {
202
+ const meta = channels.value.find(c => c.id === channelId)
203
+ settingsChannelId.value = channelId
204
+ settingsForm.name = meta?.name ?? ''
205
+ settingsForm.scenarioPrompt = meta?.scenarioPrompt ?? ''
206
+ settingsForm.workspace = meta?.workspace ?? ''
207
+ // 预填 channel 默认 LLM(llmJson 由列表接口透传)
208
+ let saved: ChannelLlmDefaults | null = null
209
+ try {
210
+ const metaAny = meta as unknown as { llmJson?: string } | undefined
211
+ saved = metaAny?.llmJson ? JSON.parse(metaAny.llmJson) as ChannelLlmDefaults : null
212
+ }
213
+ catch { saved = null }
214
+ llmForm.enabled = !!saved?.model
215
+ llmForm.harness = 'omp'
216
+ llmForm.provider = saved?.provider ?? ''
217
+ llmForm.model = saved?.model ?? ''
218
+ llmForm.effort = saved?.effort ?? ''
219
+ settingsOpen.value = true
220
+ if (llmForm.enabled) void loadLlmCatalog(llmForm.harness)
221
+ void loadChannelPlugins(channelId)
222
+ }
223
+
224
+ // ===== Channel 级插件开关(设置弹窗内嵌;切换即 PUT,该 channel 在跑 Agent 工具清单热刷新) =====
225
+ interface ChannelPluginRow { name: string, description?: string, builtin?: boolean, enabled: boolean }
226
+ const pluginRows = ref<ChannelPluginRow[]>([])
227
+ const pluginSource = ref<'explicit' | 'default'>('default')
228
+ const pluginSaving = ref<string | null>(null)
229
+ const loadChannelPlugins = async (channelId: string): Promise<void> => {
230
+ try {
231
+ const res = await api.listChannelPlugins(channelId)
232
+ const data = res?.data ?? {}
233
+ pluginRows.value = data.plugins ?? []
234
+ pluginSource.value = data.source === 'explicit' ? 'explicit' : 'default'
235
+ }
236
+ catch { /* 插件视图不可得(未登录/网络)时留空,不阻塞设置弹窗 */ }
237
+ }
238
+ const toggleChannelPlugin = async (row: ChannelPluginRow, next: boolean): Promise<void> => {
239
+ if (pluginSaving.value) return
240
+ pluginSaving.value = row.name
241
+ try {
242
+ // 全量提交当前开关视图(仅翻转目标行),与 teams 页插件开关同语义
243
+ const payload = pluginRows.value.map(r => ({ name: r.name, enabled: r.name === row.name ? next : r.enabled }))
244
+ const res = await api.putChannelPlugins(settingsChannelId.value, { plugins: payload })
245
+ const data = res?.data ?? {}
246
+ pluginRows.value = data.plugins ?? payload.map(p => ({ ...p }))
247
+ pluginSource.value = data.source === 'explicit' ? 'explicit' : 'default'
248
+ message.success(t('channelSessionList.k1plugon044'))
249
+ }
250
+ catch (e) {
251
+ message.error(apiErrorMessage(e))
252
+ }
253
+ finally {
254
+ pluginSaving.value = null
255
+ }
256
+ }
257
+ const saveSettings = async (): Promise<void> => {
258
+ settingsSaving.value = true
259
+ try {
260
+ await api.patchChannel(settingsChannelId.value, {
261
+ scenarioPrompt: settingsForm.scenarioPrompt,
262
+ workspace: settingsForm.workspace.trim() || undefined,
263
+ llm: llmForm.enabled && llmForm.model
264
+ ? { ...(llmForm.provider ? { provider: llmForm.provider } : {}), model: llmForm.model, ...(llmForm.effort ? { effort: llmForm.effort } : {}) }
265
+ : null,
266
+ })
267
+ message.success(t('channelSessionList.kvsxlu6031'))
268
+ settingsOpen.value = false
269
+ void refreshChannels()
270
+ }
271
+ catch (e) {
272
+ message.error(apiErrorMessage(e))
273
+ }
274
+ finally {
275
+ settingsSaving.value = false
276
+ }
277
+ }
278
+
279
+ // 保存为模板(捕获当前 channel 的场景/目录/团队)
280
+ const saveTplOpen = ref(false)
281
+ const saveTplForm = reactive({ name: '', description: '', visibility: 'private' as 'private' | 'public' })
282
+ const saveTplSubmitting = ref(false)
283
+ const openSaveTemplate = (): void => {
284
+ saveTplForm.name = t('channelSessionList.k2hfym5043', { p0: settingsForm.name || 'channel' })
285
+ saveTplForm.description = ''
286
+ saveTplForm.visibility = 'private'
287
+ saveTplOpen.value = true
288
+ }
289
+ const saveAsTemplate = async (): Promise<void> => {
290
+ if (!saveTplForm.name.trim()) {
291
+ message.warning(t('channelSessionList.k1i0ji0y032'))
292
+ return
293
+ }
294
+ saveTplSubmitting.value = true
295
+ try {
296
+ await api.captureChannelTemplate({
297
+ channelId: settingsChannelId.value,
298
+ name: saveTplForm.name.trim(),
299
+ description: saveTplForm.description || undefined,
300
+ visibility: saveTplForm.visibility,
301
+ })
302
+ message.success(t('channelSessionList.kjpguxw033'))
303
+ saveTplOpen.value = false
304
+ void refreshChannels()
305
+ }
306
+ catch (e) {
307
+ message.error(apiErrorMessage(e))
308
+ }
309
+ finally {
310
+ saveTplSubmitting.value = false
311
+ }
312
+ }
313
+ </script>
314
+
315
+ <template>
316
+ <div class="channel-list">
317
+ <div class="list-head">
318
+ <span class="title">Channels</span>
319
+ <span class="head-actions">
320
+ <button
321
+ class="head-op im"
322
+ type="button"
323
+ :title="$t('channelSessionList.tplCenterTitle')"
324
+ @click="navigateTo('/workshop/channel-templates')"
325
+ >
326
+ <span class="i-tabler-layout-grid-add im-pop" />
327
+ </button>
328
+ <button
329
+ class="head-op im"
330
+ type="button"
331
+ :title="$t('channelSessionList.newChannelTitle')"
332
+ @click="mountModal = true"
333
+ >
334
+ <span class="i-tabler-plus im-pop" />
335
+ </button>
336
+ </span>
337
+ </div>
338
+
339
+ <!-- 频道列表主体:数据源(REST channels + WS entities)均为客户端态,
340
+ SSR 首帧为空 → 硬刷新水合时结构与客户端首渲染不一致,节点会错配到
341
+ 相邻 antd Select 的 DOM 上(行内 .row1 布局丢失、按钮错位)。
342
+ ClientOnly 隔离:SSR 输出空壳,客户端一次性渲染,无水合配对。 -->
343
+ <ClientOnly>
344
+ <div
345
+ v-if="mountedChannels.length === 0"
346
+ class="empty"
347
+ >
348
+ {{ $t('channelSessionList.kkv789s014') }}
349
+ </div>
350
+
351
+ <div
352
+ v-for="ch in mountedChannels"
353
+ :key="ch.id"
354
+ class="channel-item"
355
+ :class="{ active: workspace?.activeChannelId === ch.id }"
356
+ @click="select(ch.id)"
357
+ >
358
+ <div class="row1">
359
+ <span
360
+ class="dot"
361
+ :class="{ live: ch.activeTasks > 0 }"
362
+ />
363
+ <span class="ch-name">{{ ch.name }}</span>
364
+ <button
365
+ class="op im"
366
+ type="button"
367
+ :title="$t('channelSessionList.kne35ug001')"
368
+ :aria-label="$t('channelSessionList.kjmldih035', { p0: ch.name })"
369
+ @click.stop="openSettings(ch.id)"
370
+ >
371
+ <span class="i-tabler-settings2 im-pop" />
372
+ </button>
373
+ <button
374
+ class="op im"
375
+ type="button"
376
+ :title="$t('channelSessionList.unmountTitle')"
377
+ :aria-label="$t('channelSessionList.k1yurooi036', { p0: ch.name })"
378
+ @click.stop="unmount(ch.id)"
379
+ >
380
+ <span class="i-tabler-x im-pop" />
381
+ </button>
382
+ </div>
383
+ <div class="row2">
384
+ <!-- 快照未到达:计数不可信,显示同步占位(不呈现误导性的"0 成员") -->
385
+ <span
386
+ v-if="!ch.synced"
387
+ class="meta syncing"
388
+ >{{ $t('channelSessionList.k1bst7s9015') }}</span>
389
+ <span
390
+ v-else
391
+ class="meta"
392
+ >{{ ch.agents }} {{ $t('channelSessionList.k1ggoa45025') }} {{ ch.busy }} / {{ $t('channelSessionList.k3wcox034') }} {{ ch.activeTasks }}</span>
393
+ </div>
394
+ <button
395
+ v-if="displayWorkspace(ch.workspace)"
396
+ class="row2 ws im"
397
+ type="button"
398
+ :title="$t('channelSessionList.kfe4pzq037', { p0: ch.workspace })"
399
+ @click.stop="copyWorkspace(ch.workspace)"
400
+ >
401
+ <span class="i-tabler-folder" /> {{ displayWorkspace(ch.workspace) }}
402
+ </button>
403
+ </div>
404
+ </ClientOnly>
405
+
406
+ <div class="mount-template">
407
+ <a-select
408
+ v-model:value="templateMountId"
409
+ size="small"
410
+ :placeholder="$t('channelSessionList.mountFromTpl')"
411
+ class="select"
412
+ :loading="templateMounting"
413
+ :options="channelTemplates.map(t => ({
414
+ value: t.id,
415
+ label: $t('channelSessionList.k1oes209038', { p0: t.isBuiltin ? $t('channelSessionList.pfxBuiltin') : t.visibility === 'public' ? $t('channelSessionList.pfxPublic') : '', p1: t.name, p2: (t.lead ? 1 : 0) + t.members.length }),
416
+ }))"
417
+ @change="mountFromTemplate"
418
+ />
419
+ <div class="tpl-hint">
420
+ {{ $t('channelSessionList.k18ppu31016') }}
421
+ </div>
422
+ </div>
423
+
424
+ <a-modal
425
+ v-model:open="mountModal"
426
+ :title="$t('channelSessionList.k1qjyl6l002')"
427
+ :confirm-loading="mountSubmitting"
428
+ :ok-text="$t('common.create')"
429
+ :cancel-text="$t('common.cancel')"
430
+ @ok="createAndMount"
431
+ >
432
+ <a-form layout="vertical">
433
+ <a-form-item :label="$t('channelSessionList.nameLabel')">
434
+ <a-input v-model:value="mountForm.name" />
435
+ </a-form-item>
436
+ <a-form-item :label="$t('channelSessionList.k40gkk003')">
437
+ <a-input v-model:value="mountForm.description" />
438
+ </a-form-item>
439
+ <a-form-item :label="$t('channelSessionList.k1wj4f38004')">
440
+ <a-textarea
441
+ v-model:value="mountForm.scenarioPrompt"
442
+ :rows="4"
443
+ :placeholder="$t('channelSessionList.k1098zie005')"
444
+ />
445
+ <template #extra>
446
+ <span class="ws-hint">{{ $t('channelSessionList.k1u3zsr5017') }}</span>
447
+ </template>
448
+ </a-form-item>
449
+ <a-form-item :label="$t('channelSessionList.kmgv4h5006')">
450
+ <a-input-group compact>
451
+ <a-input
452
+ v-model:value="mountForm.workspace"
453
+ style="width: 70%"
454
+ :placeholder="$t('channelSessionList.wsPh1')"
455
+ allow-clear
456
+ @press-enter="createAndMount"
457
+ />
458
+ <a-button
459
+ style="width: 30%"
460
+ @click="fileSelectorOpen = true"
461
+ >
462
+ {{ $t('channelSessionList.k3pz0ma018') }}
463
+ </a-button>
464
+ </a-input-group>
465
+ <template #extra>
466
+ <span class="ws-hint">{{ $t('channelSessionList.kucq30k019') }}</span>
467
+ </template>
468
+ </a-form-item>
469
+ <a-form-item>
470
+ <span class="ws-hint">{{ $t('channelSessionList.k1ful761020') }}</span>
471
+ </a-form-item>
472
+ </a-form>
473
+ </a-modal>
474
+
475
+ <!-- Channel 实例设置:场景/工作目录热更新 + 保存为模板 -->
476
+ <a-modal
477
+ v-model:open="settingsOpen"
478
+ :title="$t('channelSessionList.k1pmemvt039', { p0: settingsForm.name })"
479
+ :confirm-loading="settingsSaving"
480
+ :ok-text="$t('common.save')"
481
+ :cancel-text="$t('common.cancel')"
482
+ @ok="saveSettings"
483
+ >
484
+ <a-form layout="vertical">
485
+ <a-form-item :label="$t('channelSessionList.k1i8q46x007')">
486
+ <a-textarea
487
+ v-model:value="settingsForm.scenarioPrompt"
488
+ :rows="4"
489
+ :placeholder="$t('channelSessionList.ku3ugac008')"
490
+ />
491
+ </a-form-item>
492
+ <a-form-item :label="$t('channelSessionList.k67dzev009')">
493
+ <a-input-group compact>
494
+ <a-input
495
+ v-model:value="settingsForm.workspace"
496
+ style="width: 70%"
497
+ placeholder="data/workspaces/<channelId>"
498
+ allow-clear
499
+ />
500
+ <a-button
501
+ style="width: 30%"
502
+ @click="fileSelectorOpen2 = true"
503
+ >
504
+ {{ $t('channelSessionList.k3pz0ma018') }}
505
+ </a-button>
506
+ </a-input-group>
507
+ </a-form-item>
508
+ <a-form-item :label="$t('channelSessionList.llmDefaultLabel')">
509
+ <a-space>
510
+ <a-switch
511
+ v-model:checked="llmForm.enabled"
512
+ size="small"
513
+ @change="(v: any) => { if (v) void loadLlmCatalog(llmForm.harness) }"
514
+ />
515
+ <a-select
516
+ v-model:value="llmForm.harness"
517
+ style="width: 130px"
518
+ :options="[{ value: 'omp', label: 'omp' }, { value: 'codex', label: 'codex' }, { value: 'opencode', label: 'opencode' }, { value: 'dsh', label: 'dsh' }]"
519
+ :disabled="!llmForm.enabled"
520
+ @change="onLlmHarnessChange"
521
+ />
522
+ <a-select
523
+ v-model:value="llmForm.provider"
524
+ style="width: 170px"
525
+ :placeholder="llmLoading ? $t('channelSessionList.catalogLoading') : 'provider'"
526
+ :options="llmProviderOptions"
527
+ :disabled="!llmForm.enabled || llmLoading"
528
+ show-search
529
+ @change="onLlmProviderChange"
530
+ />
531
+ <a-select
532
+ v-model:value="llmForm.model"
533
+ style="width: 210px"
534
+ placeholder="model"
535
+ :options="llmModelOptions"
536
+ :disabled="!llmForm.enabled || !llmForm.provider"
537
+ show-search
538
+ @change="onLlmModelChange"
539
+ />
540
+ <a-select
541
+ v-if="llmEffortMode === 'levels'"
542
+ v-model:value="llmForm.effort"
543
+ style="width: 130px"
544
+ :placeholder="$t('channelSessionList.effortPh')"
545
+ :options="llmEffortOptions"
546
+ :disabled="!llmForm.enabled || !llmForm.model"
547
+ allow-clear
548
+ />
549
+ <a-input
550
+ v-else-if="llmEffortMode === 'freetext'"
551
+ v-model:value="llmForm.effort"
552
+ style="width: 130px"
553
+ :placeholder="$t('channelSessionList.variantPh')"
554
+ :disabled="!llmForm.enabled || !llmForm.model"
555
+ allow-clear
556
+ />
557
+ </a-space>
558
+ </a-form-item>
559
+ <a-form-item :label="t('channelSessionList.k1pluglbl045')">
560
+ <div
561
+ v-if="pluginRows.length"
562
+ class="ch-plugin-list"
563
+ >
564
+ <div
565
+ v-for="p in pluginRows"
566
+ :key="p.name"
567
+ class="ch-plugin-row"
568
+ >
569
+ <div class="ch-plugin-main">
570
+ <span class="aw-mono">{{ p.name }}</span>
571
+ <span
572
+ v-if="p.builtin"
573
+ class="ws-hint"
574
+ >{{ $t('channelSessionList.builtinTag') }}</span>
575
+ <span
576
+ v-if="p.description"
577
+ class="ws-hint ch-plugin-desc"
578
+ >{{ p.description }}</span>
579
+ </div>
580
+ <a-switch
581
+ :checked="p.enabled"
582
+ size="small"
583
+ :loading="pluginSaving === p.name"
584
+ @change="(v: any) => toggleChannelPlugin(p, !!v)"
585
+ />
586
+ </div>
587
+ <span
588
+ v-if="pluginSource !== 'explicit'"
589
+ class="ws-hint"
590
+ >{{ t('channelSessionList.k1plugsrc046') }}</span>
591
+ </div>
592
+ <span
593
+ v-else
594
+ class="ws-hint"
595
+ >—</span>
596
+ </a-form-item>
597
+ <a-form-item>
598
+ <a-button
599
+ size="small"
600
+ @click="openSaveTemplate"
601
+ >
602
+ <span class="i-tabler-template" />
603
+ {{ $t('channelSessionList.k6kpql010') }}
604
+ </a-button>
605
+ <span class="ws-hint">{{ $t('channelSessionList.k1x6kznr021') }}</span>
606
+ </a-form-item>
607
+ </a-form>
608
+ </a-modal>
609
+
610
+ <!-- 保存为模板 -->
611
+ <a-modal
612
+ v-model:open="saveTplOpen"
613
+ :title="$t('channelSessionList.k6kpql010')"
614
+ :confirm-loading="saveTplSubmitting"
615
+ :ok-text="$t('channelSessionList.saveTplOk')"
616
+ :cancel-text="$t('common.cancel')"
617
+ @ok="saveAsTemplate"
618
+ >
619
+ <a-form layout="vertical">
620
+ <a-form-item :label="$t('channelSessionList.k1f55q76011')">
621
+ <a-input v-model:value="saveTplForm.name" />
622
+ </a-form-item>
623
+ <a-form-item :label="$t('channelSessionList.k40gkk003')">
624
+ <a-input v-model:value="saveTplForm.description" />
625
+ </a-form-item>
626
+ <a-form-item :label="$t('channelSessionList.k3lrqn0012')">
627
+ <a-radio-group v-model:value="saveTplForm.visibility">
628
+ <a-radio value="private">
629
+ {{ $t('channelSessionList.k1otvrfv022') }}
630
+ </a-radio>
631
+ <a-radio value="public">
632
+ {{ $t('channelSessionList.k1cc399s023') }}
633
+ </a-radio>
634
+ </a-radio-group>
635
+ </a-form-item>
636
+ <a-form-item>
637
+ <span class="ws-hint">{{ $t('channelSessionList.kl9btf7024') }}</span>
638
+ </a-form-item>
639
+ </a-form>
640
+ </a-modal>
641
+
642
+ <!-- FileSelector:服务器目录选择 -> 回填工作目录 -->
643
+ <workshop-file-selector-modal
644
+ v-model:open="fileSelectorOpen"
645
+ :title="$t('channelSessionList.kbdp56k013')"
646
+ :initial-path="mountForm.workspace || undefined"
647
+ @select="(p) => { mountForm.workspace = p }"
648
+ />
649
+ <workshop-file-selector-modal
650
+ v-model:open="fileSelectorOpen2"
651
+ :title="$t('channelSessionList.kbdp56k013')"
652
+ :initial-path="settingsForm.workspace || undefined"
653
+ @select="(p) => { settingsForm.workspace = p }"
654
+ />
655
+ </div>
656
+ </template>
657
+
658
+ <style scoped>
659
+ .ch-plugin-list {
660
+ display: flex;
661
+ flex-direction: column;
662
+ gap: 6px;
663
+ width: 100%;
664
+ }
665
+ .ch-plugin-row {
666
+ display: flex;
667
+ align-items: center;
668
+ justify-content: space-between;
669
+ gap: 12px;
670
+ padding: 4px 8px;
671
+ border: 1px solid var(--border, rgba(128, 128, 128, 0.25));
672
+ border-radius: 6px;
673
+ }
674
+ .ch-plugin-main {
675
+ display: flex;
676
+ align-items: baseline;
677
+ gap: 8px;
678
+ min-width: 0;
679
+ }
680
+ .ch-plugin-desc {
681
+ overflow: hidden;
682
+ text-overflow: ellipsis;
683
+ white-space: nowrap;
684
+ }
685
+ .channel-list {
686
+ display: flex;
687
+ flex-direction: column;
688
+ min-height: 100%;
689
+ padding: 12px 10px 8px;
690
+ }
691
+ .list-head {
692
+ display: flex;
693
+ align-items: center;
694
+ justify-content: space-between;
695
+ padding: 0 8px 8px;
696
+ }
697
+ .title {
698
+ font-family: var(--font-display);
699
+ font-size: 16px;
700
+ letter-spacing: -0.01em;
701
+ color: var(--ink);
702
+ }
703
+ .head-actions { display: inline-flex; gap: 2px; }
704
+ .head-op {
705
+ display: inline-flex;
706
+ align-items: center;
707
+ justify-content: center;
708
+ width: 24px;
709
+ height: 24px;
710
+ font-size: 14px;
711
+ color: var(--ink-faint);
712
+ cursor: pointer;
713
+ background: transparent;
714
+ border: 0;
715
+ border-radius: var(--radius-chip);
716
+ transition: background var(--transition-fast), color var(--transition-fast);
717
+ }
718
+ .head-op:hover { color: var(--ink); background: var(--paper-deep); }
719
+ .empty {
720
+ padding: 14px 10px;
721
+ font-size: 12px;
722
+ color: var(--ink-faint);
723
+ }
724
+ .channel-item {
725
+ padding: 7px 9px;
726
+ margin: 1px 0;
727
+ cursor: pointer;
728
+ border-radius: var(--radius-panel-sm);
729
+ transition: background var(--transition-fast);
730
+ }
731
+ .channel-item:hover { background: var(--paper-deep); }
732
+ .channel-item.active { background: var(--paper-deep); box-shadow: inset 2px 0 0 var(--accent); }
733
+ .row1 {
734
+ display: flex;
735
+ gap: 7px;
736
+ align-items: center;
737
+ font-size: 13px;
738
+ color: var(--ink-soft);
739
+ }
740
+ .channel-item.active .row1 { color: var(--ink); font-weight: 500; }
741
+ .dot {
742
+ flex: 0 0 auto;
743
+ width: 7px;
744
+ height: 7px;
745
+ background: var(--tone-neutral-dot);
746
+ opacity: 0.55;
747
+ border-radius: 50%;
748
+ }
749
+ .dot.live {
750
+ background: var(--tone-success-dot);
751
+ opacity: 1;
752
+ }
753
+ .ch-name {
754
+ flex: 1 1 auto;
755
+ overflow: hidden;
756
+ text-overflow: ellipsis;
757
+ white-space: nowrap;
758
+ }
759
+ .op {
760
+ display: inline-flex;
761
+ flex: 0 0 auto;
762
+ align-items: center;
763
+ justify-content: center;
764
+ width: 20px;
765
+ height: 20px;
766
+ padding: 0;
767
+ font-size: 13px;
768
+ color: var(--ink-faint);
769
+ cursor: pointer;
770
+ background: transparent;
771
+ border: 0;
772
+ border-radius: var(--radius-chip);
773
+ /* 常显(低透明度):不再 hover-only——行级操作入口不应时隐时现 */
774
+ opacity: 0.55;
775
+ transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
776
+ }
777
+ .channel-item:hover .op,
778
+ .channel-item.active .op { opacity: 0.9; }
779
+ .op:hover { opacity: 1; color: var(--ink); background: var(--paper-deep); }
780
+ .op:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
781
+ .row2 { padding-left: 14px; font-size: 11px; color: var(--ink-faint); }
782
+ .meta.syncing { font-style: italic; opacity: 0.6; }
783
+ .row2.ws {
784
+ display: flex;
785
+ gap: 4px;
786
+ align-items: center;
787
+ overflow: hidden;
788
+ max-width: 100%;
789
+ padding: 0 2px 0 14px;
790
+ font-family: var(--font-mono);
791
+ font-size: 11px;
792
+ text-overflow: ellipsis;
793
+ white-space: nowrap;
794
+ color: var(--ink-faint);
795
+ text-align: left;
796
+ cursor: copy; /* 点击复制完整路径(title 提示) */
797
+ background: transparent;
798
+ border: 0;
799
+ border-radius: var(--radius-chip);
800
+ transition: color var(--transition-fast), background var(--transition-fast);
801
+ }
802
+ .row2.ws:hover { color: var(--ink-soft); background: color-mix(in srgb, currentColor 6%, transparent); }
803
+ .row2.ws:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
804
+ .mount-template { padding: 12px 6px 6px; border-top: 1px solid var(--line); margin-top: 10px; }
805
+ .select { width: 100%; }
806
+ .tpl-hint {
807
+ padding: 5px 2px 0;
808
+ font-size: 10.5px;
809
+ color: var(--ink-faint);
810
+ }
811
+ .ws-hint { font-size: 11px; color: var(--ink-faint); }
812
+ </style>