agentworkshop 0.2.1

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 (895) hide show
  1. package/.AgentWorkShop/prompts/host-tools.json +631 -0
  2. package/.AgentWorkShop/prompts/lead-supervise.md +42 -0
  3. package/.AgentWorkShop/prompts/mode-goal.md +26 -0
  4. package/.AgentWorkShop/prompts/mode-loop.md +10 -0
  5. package/.AgentWorkShop/prompts/mode-pipeline.md +16 -0
  6. package/.AgentWorkShop/prompts/peer-lead-roster.md +3 -0
  7. package/.AgentWorkShop/prompts/peer-message.md +14 -0
  8. package/.AgentWorkShop/prompts/peer-reply-optional.md +2 -0
  9. package/.AgentWorkShop/prompts/peer-reply-required.md +8 -0
  10. package/.AgentWorkShop/prompts/scenario-default.md +11 -0
  11. package/.AgentWorkShop/prompts/system-manual.md +18 -0
  12. package/.AgentWorkShop/prompts/team-roster.md +9 -0
  13. package/.AgentWorkShop/prompts/worker-workflow.md +19 -0
  14. package/.env.example +34 -0
  15. package/README-zh.md +424 -0
  16. package/README.md +424 -0
  17. package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.json +1 -0
  18. package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.mtg0vz68-49792-75b0709555.baseline/1459e6269990a5d229b9228c8b2f62ffaa7d350068380ae8b875fee0db45cca6.source +180 -0
  19. package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.mtg0vz68-49792-75b0709555.baseline/c2991e909c70c5c12b90a407c7adcf63384c51668a2b334e6e047fa675c268fe.source +305 -0
  20. package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.mtg0vz68-49792-75b0709555.baseline/e09129039d1e27e3fac24becc9bdafd2d1a7c8049bff24173df007be865f0165.source +264 -0
  21. package/app/.mimosa/hook-status/sess_05189059-e42d-4188-8911-4f7baabe009b-28ef7c75ea.json +14 -0
  22. package/app/app.vue +139 -0
  23. package/app/assets/css/main.css +1257 -0
  24. package/app/components/AppHeader.vue +675 -0
  25. package/app/components/AppSidebar.vue +286 -0
  26. package/app/components/AwChart.vue +44 -0
  27. package/app/components/AwLogo.vue +75 -0
  28. package/app/components/workshop/A2aRpcDebugger.vue +237 -0
  29. package/app/components/workshop/AgentInspectorDrawer.vue +424 -0
  30. package/app/components/workshop/AgentLanesView.vue +947 -0
  31. package/app/components/workshop/ArtifactCard.vue +120 -0
  32. package/app/components/workshop/AssetLibrary.vue +450 -0
  33. package/app/components/workshop/ChannelSessionList.vue +593 -0
  34. package/app/components/workshop/CommandPalette.vue +270 -0
  35. package/app/components/workshop/Composer.vue +731 -0
  36. package/app/components/workshop/DeviceTwinPanel.vue +607 -0
  37. package/app/components/workshop/EventBlock.vue +602 -0
  38. package/app/components/workshop/FileSelectorModal.vue +188 -0
  39. package/app/components/workshop/InspectorPanel.vue +265 -0
  40. package/app/components/workshop/LiveAgentBar.vue +362 -0
  41. package/app/components/workshop/MemoryPanel.vue +364 -0
  42. package/app/components/workshop/MentionHoverCard.vue +155 -0
  43. package/app/components/workshop/MultiChannelView.vue +189 -0
  44. package/app/components/workshop/PaneSplitter.vue +142 -0
  45. package/app/components/workshop/StatsPanel.vue +168 -0
  46. package/app/components/workshop/TaskBoardView.vue +636 -0
  47. package/app/components/workshop/TaskInspectorDrawer.vue +390 -0
  48. package/app/components/workshop/TranscriptTimeline.vue +545 -0
  49. package/app/components/workshop/blocks/ClusterArtifact.vue +77 -0
  50. package/app/components/workshop/blocks/ClusterError.vue +85 -0
  51. package/app/components/workshop/blocks/ClusterLife.vue +87 -0
  52. package/app/components/workshop/blocks/ClusterMember.vue +93 -0
  53. package/app/components/workshop/blocks/ClusterMemory.vue +84 -0
  54. package/app/components/workshop/blocks/ClusterOther.vue +67 -0
  55. package/app/components/workshop/blocks/ClusterRoute.vue +359 -0
  56. package/app/components/workshop/blocks/ClusterStatus.vue +147 -0
  57. package/app/components/workshop/blocks/ClusterStream.vue +325 -0
  58. package/app/components/workshop/blocks/ClusterTask.vue +151 -0
  59. package/app/components/workshop/blocks/ClusterTool.vue +158 -0
  60. package/app/components/workshop/lanes/LaneBlocks.vue +149 -0
  61. package/app/components/workshop/terminal/OmpTerminalPanel.vue +1010 -0
  62. package/app/components/workshop/town/ModelPreview3D.vue +276 -0
  63. package/app/components/workshop/town/TownScene.ts +1355 -0
  64. package/app/components/workshop/town/TownScene3D.ts +4533 -0
  65. package/app/components/workshop/town/TownView.vue +7071 -0
  66. package/app/composables/useCodeCopy.ts +30 -0
  67. package/app/composables/useHttp.ts +8 -0
  68. package/app/composables/useRouteMeta.ts +50 -0
  69. package/app/composables/useSiteConfig.ts +21 -0
  70. package/app/composables/workshop/apiClient.ts +95 -0
  71. package/app/composables/workshop/useCharacterAssets.ts +292 -0
  72. package/app/composables/workshop/useClusteredBlocks.ts +150 -0
  73. package/app/composables/workshop/useComposerBus.ts +16 -0
  74. package/app/composables/workshop/useDaqStream.ts +379 -0
  75. package/app/composables/workshop/useDcwStream.ts +300 -0
  76. package/app/composables/workshop/useDeviceTwins.ts +147 -0
  77. package/app/composables/workshop/useEventBlocks.ts +662 -0
  78. package/app/composables/workshop/useLocalTime.ts +33 -0
  79. package/app/composables/workshop/useOpsLog.ts +164 -0
  80. package/app/composables/workshop/useSceneLayouts.ts +166 -0
  81. package/app/composables/workshop/useTownBus.ts +47 -0
  82. package/app/composables/workshop/useVisibleInterval.ts +42 -0
  83. package/app/composables/workshop/useWorkshopApi.ts +224 -0
  84. package/app/composables/workshop/useWorkshopWs.ts +113 -0
  85. package/app/config/index.ts +66 -0
  86. package/app/config/schema.ts +68 -0
  87. package/app/error.vue +76 -0
  88. package/app/layouts/default.vue +100 -0
  89. package/app/layouts/town.vue +16 -0
  90. package/app/pages/daq/[id].vue +796 -0
  91. package/app/pages/daq/index.vue +2577 -0
  92. package/app/pages/dcw/[id].vue +2259 -0
  93. package/app/pages/dcw/index.vue +1079 -0
  94. package/app/pages/index.vue +830 -0
  95. package/app/pages/logs/index.vue +621 -0
  96. package/app/pages/monitor.vue +665 -0
  97. package/app/pages/settings.vue +757 -0
  98. package/app/pages/tokens.vue +610 -0
  99. package/app/pages/town.vue +145 -0
  100. package/app/pages/users.vue +257 -0
  101. package/app/pages/workshop/agents.vue +382 -0
  102. package/app/pages/workshop/channel-templates.vue +456 -0
  103. package/app/pages/workshop/index.vue +604 -0
  104. package/app/pages/workshop/teams.vue +448 -0
  105. package/app/pages/workshop/w/[wsId].vue +516 -0
  106. package/app/plugins/http.ts +71 -0
  107. package/app/plugins/locale-restore.client.ts +13 -0
  108. package/app/plugins/runtime-config.client.ts +65 -0
  109. package/app/plugins/session-restore.client.ts +24 -0
  110. package/app/plugins/theme.client.ts +15 -0
  111. package/app/stores/app.ts +32 -0
  112. package/app/stores/route-trail.ts +45 -0
  113. package/app/stores/runtime-config.ts +212 -0
  114. package/app/stores/workshop/connection.ts +184 -0
  115. package/app/stores/workshop/entities.ts +350 -0
  116. package/app/stores/workshop/events.ts +292 -0
  117. package/app/stores/workshop/user.ts +176 -0
  118. package/app/stores/workshop/workspaces.ts +133 -0
  119. package/bin/aw.mjs +13 -0
  120. package/cli/aw.mjs +184 -0
  121. package/cli/commands/build.mjs +33 -0
  122. package/cli/commands/config.mjs +157 -0
  123. package/cli/commands/dev.mjs +74 -0
  124. package/cli/commands/doctor.mjs +120 -0
  125. package/cli/commands/home.mjs +75 -0
  126. package/cli/commands/init.mjs +150 -0
  127. package/cli/commands/register.mjs +123 -0
  128. package/cli/commands/start.mjs +106 -0
  129. package/cli/commands/status.mjs +67 -0
  130. package/cli/commands/update.mjs +116 -0
  131. package/cli/commands/version.mjs +32 -0
  132. package/cli/core/args.mjs +95 -0
  133. package/cli/core/context.mjs +264 -0
  134. package/cli/core/errors.mjs +18 -0
  135. package/cli/core/help.mjs +68 -0
  136. package/cli/core/logger.mjs +40 -0
  137. package/cli/core/registry.mjs +111 -0
  138. package/config.yml +70 -0
  139. package/docker-compose.yml +34 -0
  140. package/docs/cli.md +220 -0
  141. package/i18n/dicts/_en.json +1097 -0
  142. package/i18n/dicts/_en2.json +300 -0
  143. package/i18n/dicts/_en3.json +102 -0
  144. package/i18n/dicts/_en4.json +115 -0
  145. package/i18n/dicts/_en5.json +33 -0
  146. package/i18n/dicts/a2aRpcDebugger.json +10 -0
  147. package/i18n/dicts/agentInspectorDrawer.json +25 -0
  148. package/i18n/dicts/agentLanesView.json +57 -0
  149. package/i18n/dicts/agents.json +27 -0
  150. package/i18n/dicts/appHeader.json +7 -0
  151. package/i18n/dicts/appSidebar.json +1 -0
  152. package/i18n/dicts/artifactCard.json +6 -0
  153. package/i18n/dicts/assetLibrary.json +18 -0
  154. package/i18n/dicts/awChart.json +1 -0
  155. package/i18n/dicts/awLogo.json +1 -0
  156. package/i18n/dicts/channelSessionList.json +45 -0
  157. package/i18n/dicts/clusterArtifact.json +4 -0
  158. package/i18n/dicts/clusterError.json +3 -0
  159. package/i18n/dicts/clusterLife.json +5 -0
  160. package/i18n/dicts/clusterMember.json +3 -0
  161. package/i18n/dicts/clusterMemory.json +3 -0
  162. package/i18n/dicts/clusterOther.json +3 -0
  163. package/i18n/dicts/clusterRoute.json +15 -0
  164. package/i18n/dicts/clusterStatus.json +3 -0
  165. package/i18n/dicts/clusterStream.json +3 -0
  166. package/i18n/dicts/clusterTask.json +10 -0
  167. package/i18n/dicts/clusterTool.json +3 -0
  168. package/i18n/dicts/commandPalette.json +16 -0
  169. package/i18n/dicts/composer.json +38 -0
  170. package/i18n/dicts/ctpl.json +32 -0
  171. package/i18n/dicts/daq.json +157 -0
  172. package/i18n/dicts/daqDetail.json +55 -0
  173. package/i18n/dicts/dcw.json +68 -0
  174. package/i18n/dicts/dcwDetail.json +199 -0
  175. package/i18n/dicts/deviceTwinPanel.json +12 -0
  176. package/i18n/dicts/eventBlock.json +8 -0
  177. package/i18n/dicts/fileSelectorModal.json +6 -0
  178. package/i18n/dicts/inspectorPanel.json +12 -0
  179. package/i18n/dicts/laneBlocks.json +6 -0
  180. package/i18n/dicts/liveAgentBar.json +8 -0
  181. package/i18n/dicts/logs.json +39 -0
  182. package/i18n/dicts/memoryPanel.json +20 -0
  183. package/i18n/dicts/mentionHoverCard.json +4 -0
  184. package/i18n/dicts/modelPreview3D.json +1 -0
  185. package/i18n/dicts/monitor.json +5 -0
  186. package/i18n/dicts/multiChannelView.json +13 -0
  187. package/i18n/dicts/ompTerminalPanel.json +7 -0
  188. package/i18n/dicts/paneSplitter.json +3 -0
  189. package/i18n/dicts/settings.json +1 -0
  190. package/i18n/dicts/statsPanel.json +6 -0
  191. package/i18n/dicts/taskBoardView.json +22 -0
  192. package/i18n/dicts/taskInspectorDrawer.json +19 -0
  193. package/i18n/dicts/teams.json +33 -0
  194. package/i18n/dicts/tokens.json +30 -0
  195. package/i18n/dicts/town.json +7 -0
  196. package/i18n/dicts/townView.json +209 -0
  197. package/i18n/dicts/transcriptTimeline.json +31 -0
  198. package/i18n/dicts/users.json +15 -0
  199. package/i18n/dicts/wsHome.json +33 -0
  200. package/i18n/dicts/wsView.json +15 -0
  201. package/i18n/locales/en.ts +1737 -0
  202. package/i18n/locales/zh-CN.ts +1736 -0
  203. package/nuxt.config.ts +209 -0
  204. package/package.json +123 -0
  205. package/public/assets/game/character/bot.png +0 -0
  206. package/public/assets/game/character/device-3d.glb +0 -0
  207. package/public/assets/game/character/hero-3d.glb +0 -0
  208. package/public/assets/game/character/hero-anime-1.glb +0 -0
  209. package/public/assets/game/character/hero-anime-2.glb +0 -0
  210. package/public/assets/game/character/hero-anime-3.glb +0 -0
  211. package/public/assets/game/character/hero-anime-4.glb +0 -0
  212. package/public/assets/game/character/knight.png +0 -0
  213. package/public/assets/game/character/mage.png +0 -0
  214. package/public/assets/game/coin.png +0 -0
  215. package/public/assets/game/devices/agv.glb +0 -0
  216. package/public/assets/game/devices/caster.glb +0 -0
  217. package/public/assets/game/devices/device-console.glb +0 -0
  218. package/public/assets/game/devices/device-robot-arm.glb +0 -0
  219. package/public/assets/game/devices/device-scanner.glb +0 -0
  220. package/public/assets/game/devices/extruder.glb +0 -0
  221. package/public/assets/game/devices/mdo.glb +0 -0
  222. package/public/assets/game/devices/power-cabinet.glb +0 -0
  223. package/public/assets/game/devices/pump.glb +0 -0
  224. package/public/assets/game/devices/tdo.glb +0 -0
  225. package/public/assets/game/devices/thickness-scanner.glb +0 -0
  226. package/public/assets/game/devices/winder.glb +0 -0
  227. package/public/assets/game/npc/boltnu-sheet.png +0 -0
  228. package/public/assets/game/npc/fluttaflap-sheet.png +0 -0
  229. package/public/assets/game/npc/rockitten-sheet.png +0 -0
  230. package/public/assets/game/npc/tux-sheet.png +0 -0
  231. package/public/assets/game/player/misa-back-walk.000.png +0 -0
  232. package/public/assets/game/player/misa-back-walk.001.png +0 -0
  233. package/public/assets/game/player/misa-back-walk.002.png +0 -0
  234. package/public/assets/game/player/misa-back-walk.003.png +0 -0
  235. package/public/assets/game/player/misa-back.png +0 -0
  236. package/public/assets/game/player/misa-front-walk.000.png +0 -0
  237. package/public/assets/game/player/misa-front-walk.001.png +0 -0
  238. package/public/assets/game/player/misa-front-walk.002.png +0 -0
  239. package/public/assets/game/player/misa-front-walk.003.png +0 -0
  240. package/public/assets/game/player/misa-front.png +0 -0
  241. package/public/assets/game/player/misa-left-walk.000.png +0 -0
  242. package/public/assets/game/player/misa-left-walk.001.png +0 -0
  243. package/public/assets/game/player/misa-left-walk.002.png +0 -0
  244. package/public/assets/game/player/misa-left-walk.003.png +0 -0
  245. package/public/assets/game/player/misa-left.png +0 -0
  246. package/public/assets/game/player/misa-right-walk.000.png +0 -0
  247. package/public/assets/game/player/misa-right-walk.001.png +0 -0
  248. package/public/assets/game/player/misa-right-walk.002.png +0 -0
  249. package/public/assets/game/player/misa-right-walk.003.png +0 -0
  250. package/public/assets/game/player/misa-right.png +0 -0
  251. package/public/assets/game/player-sheet.png +0 -0
  252. package/public/assets/game/tuxemon-town.json +1 -0
  253. package/public/assets/game/tuxmon-sample-32px-extruded.png +0 -0
  254. package/public/assets/game/wuwa/scene-map.png +0 -0
  255. package/public/assets/game/wuwa/world-far.png +0 -0
  256. package/public/assets/game/wuwa/world-middle.png +0 -0
  257. package/public/assets/game/wuwa/world-near.png +0 -0
  258. package/public/assets/game/wuwa/wu-aura.png +0 -0
  259. package/public/assets/game/wuwa/wu-lead.png +0 -0
  260. package/public/assets/game/wuwa/wu-ring.png +0 -0
  261. package/public/assets/game/wuwa/wu-slash.png +0 -0
  262. package/public/assets/game/wuwa/wu-worker-0.png +0 -0
  263. package/public/assets/game/wuwa/wu-worker-1.png +0 -0
  264. package/public/assets/game/wuwa/wu-worker-2.png +0 -0
  265. package/public/favicon.svg +14 -0
  266. package/public/scene/background/cyber-town-background.svg +110 -0
  267. package/scripts/_daq-realtime-probe.mjs +68 -0
  268. package/scripts/_daq-warn-probe.mjs +50 -0
  269. package/scripts/_dbg-agent-tools-audit.mjs +154 -0
  270. package/scripts/_dbg-agent-workflow-audit.mjs +177 -0
  271. package/scripts/_dbg-anim-verify.mjs +81 -0
  272. package/scripts/_dbg-audit-all-pages.mjs +43 -0
  273. package/scripts/_dbg-autoframe.mjs +103 -0
  274. package/scripts/_dbg-beacon-final.mjs +181 -0
  275. package/scripts/_dbg-bind-bidi-e2e.mjs +239 -0
  276. package/scripts/_dbg-callout-near.mjs +108 -0
  277. package/scripts/_dbg-chan-focus.mjs +49 -0
  278. package/scripts/_dbg-cleanup.mjs +31 -0
  279. package/scripts/_dbg-closedloop-ui-shot.mjs +61 -0
  280. package/scripts/_dbg-console-audit.mjs +61 -0
  281. package/scripts/_dbg-console-probe.mjs +23 -0
  282. package/scripts/_dbg-context-inject-audit.mjs +231 -0
  283. package/scripts/_dbg-crash-recovery.mjs +143 -0
  284. package/scripts/_dbg-crop-shot.mjs +16 -0
  285. package/scripts/_dbg-daq-acc-probe.mjs +24 -0
  286. package/scripts/_dbg-daq-attrib.mjs +34 -0
  287. package/scripts/_dbg-daq-bind-audit.mjs +119 -0
  288. package/scripts/_dbg-daq-cadence.mjs +104 -0
  289. package/scripts/_dbg-daq-debug.mjs +22 -0
  290. package/scripts/_dbg-daq-diag.mjs +33 -0
  291. package/scripts/_dbg-daq-e2e.mjs +171 -0
  292. package/scripts/_dbg-daq-film.mjs +136 -0
  293. package/scripts/_dbg-daq-filter-e2e.mjs +109 -0
  294. package/scripts/_dbg-daq-filter-e2e2.mjs +62 -0
  295. package/scripts/_dbg-daq-final-regression.mjs +78 -0
  296. package/scripts/_dbg-daq-full-check.mjs +179 -0
  297. package/scripts/_dbg-daq-line-note-verify.mjs +61 -0
  298. package/scripts/_dbg-daq-linebanner-audit.mjs +107 -0
  299. package/scripts/_dbg-daq-pages-shot.mjs +37 -0
  300. package/scripts/_dbg-daq-perf.mjs +61 -0
  301. package/scripts/_dbg-daq-probe.mjs +27 -0
  302. package/scripts/_dbg-daq-recipe-e2e.mjs +30 -0
  303. package/scripts/_dbg-daq-redesign-shot.mjs +88 -0
  304. package/scripts/_dbg-daq-rows-probe.mjs +26 -0
  305. package/scripts/_dbg-daq-rowstate-e2e.mjs +152 -0
  306. package/scripts/_dbg-daq-rowstate-shot.mjs +45 -0
  307. package/scripts/_dbg-daq-rowstate-shot2.mjs +38 -0
  308. package/scripts/_dbg-daq-tpl.mjs +144 -0
  309. package/scripts/_dbg-dash-console-check.mjs +49 -0
  310. package/scripts/_dbg-dash-deep-probe.mjs +61 -0
  311. package/scripts/_dbg-dash-hitpoint.mjs +44 -0
  312. package/scripts/_dbg-dash-live-shot.mjs +36 -0
  313. package/scripts/_dbg-dash-shot.mjs +21 -0
  314. package/scripts/_dbg-dashboard-shot.mjs +35 -0
  315. package/scripts/_dbg-dcw-audit.mjs +126 -0
  316. package/scripts/_dbg-dcw-card-e2e.mjs +108 -0
  317. package/scripts/_dbg-dcw-channel-ui.mjs +144 -0
  318. package/scripts/_dbg-dcw-examples.mjs +81 -0
  319. package/scripts/_dbg-dcw-light-shot.mjs +19 -0
  320. package/scripts/_dbg-dcw-line-audit.mjs +107 -0
  321. package/scripts/_dbg-dcw-pause-tpl-audit.mjs +161 -0
  322. package/scripts/_dbg-dcw-pause-ui.mjs +175 -0
  323. package/scripts/_dbg-dcw-scene.mjs +72 -0
  324. package/scripts/_dbg-dcw-shot2.mjs +14 -0
  325. package/scripts/_dbg-dcw-twin-e2e.mjs +192 -0
  326. package/scripts/_dbg-dt-interact.mjs +193 -0
  327. package/scripts/_dbg-error-probe.mjs +54 -0
  328. package/scripts/_dbg-filter-light-verify.mjs +54 -0
  329. package/scripts/_dbg-final-acceptance-2.mjs +72 -0
  330. package/scripts/_dbg-final-acceptance.mjs +192 -0
  331. package/scripts/_dbg-final-agent-e2e.mjs +151 -0
  332. package/scripts/_dbg-final-full-e2e.mjs +218 -0
  333. package/scripts/_dbg-final-line-e2e.mjs +173 -0
  334. package/scripts/_dbg-final-t6-hitl.mjs +38 -0
  335. package/scripts/_dbg-focus-range.mjs +115 -0
  336. package/scripts/_dbg-fresh-scene-e2e.mjs +125 -0
  337. package/scripts/_dbg-full-feature-e2e.mjs +219 -0
  338. package/scripts/_dbg-fullchain-e2e.mjs +178 -0
  339. package/scripts/_dbg-gate-debug.mjs +37 -0
  340. package/scripts/_dbg-gif-encode.mjs +111 -0
  341. package/scripts/_dbg-gizmo.mjs +95 -0
  342. package/scripts/_dbg-glass-probe.mjs +57 -0
  343. package/scripts/_dbg-glass-shot.mjs +50 -0
  344. package/scripts/_dbg-glass-sweep.mjs +25 -0
  345. package/scripts/_dbg-glb-probe.mjs +43 -0
  346. package/scripts/_dbg-hardening-probe.mjs +90 -0
  347. package/scripts/_dbg-hero-btn-probe.mjs +21 -0
  348. package/scripts/_dbg-hidden-check.mjs +21 -0
  349. package/scripts/_dbg-hitl-timeout-e2e.mjs +93 -0
  350. package/scripts/_dbg-hmi-audit.mjs +164 -0
  351. package/scripts/_dbg-home-repro.mjs +24 -0
  352. package/scripts/_dbg-home-stack.mjs +13 -0
  353. package/scripts/_dbg-huashu-survey.mjs +31 -0
  354. package/scripts/_dbg-huashu-survey2.mjs +35 -0
  355. package/scripts/_dbg-huashu-verify.mjs +25 -0
  356. package/scripts/_dbg-huashu-verify2.mjs +26 -0
  357. package/scripts/_dbg-hydration-detail.mjs +18 -0
  358. package/scripts/_dbg-i18n-debug.mjs +27 -0
  359. package/scripts/_dbg-i18n-debug2.mjs +26 -0
  360. package/scripts/_dbg-i18n-final.mjs +42 -0
  361. package/scripts/_dbg-i18n-scan.mjs +38 -0
  362. package/scripts/_dbg-infra-lifecycle.mjs +44 -0
  363. package/scripts/_dbg-line-aura-shots.mjs +54 -0
  364. package/scripts/_dbg-line-control-e2e.mjs +120 -0
  365. package/scripts/_dbg-line-gate-audit.mjs +108 -0
  366. package/scripts/_dbg-line-live-e2e.mjs +144 -0
  367. package/scripts/_dbg-line-scenario-e2e.mjs +256 -0
  368. package/scripts/_dbg-minimal-omp.mjs +77 -0
  369. package/scripts/_dbg-mode-nav.mjs +173 -0
  370. package/scripts/_dbg-mqtt-chain-e2e.mjs +90 -0
  371. package/scripts/_dbg-nav-rpg.mjs +84 -0
  372. package/scripts/_dbg-nav-shot.mjs +28 -0
  373. package/scripts/_dbg-nav.mjs +27 -0
  374. package/scripts/_dbg-opslog-e2e.mjs +121 -0
  375. package/scripts/_dbg-opslog-shots.mjs +62 -0
  376. package/scripts/_dbg-opt-team-e2e.mjs +133 -0
  377. package/scripts/_dbg-optimization-e2e.mjs +94 -0
  378. package/scripts/_dbg-orbit.mjs +58 -0
  379. package/scripts/_dbg-panesplitter-fix-check.mjs +62 -0
  380. package/scripts/_dbg-phase-e3-monitor.mjs +25 -0
  381. package/scripts/_dbg-pop-clamp-verify.mjs +77 -0
  382. package/scripts/_dbg-preset-e2e.mjs +116 -0
  383. package/scripts/_dbg-preview-console.mjs +28 -0
  384. package/scripts/_dbg-preview-minrepro.mjs +156 -0
  385. package/scripts/_dbg-preview-pixels.mjs +93 -0
  386. package/scripts/_dbg-quality-pages-e2e.mjs +84 -0
  387. package/scripts/_dbg-query-cancel.ts +12 -0
  388. package/scripts/_dbg-rail-tree-e2e.mjs +183 -0
  389. package/scripts/_dbg-real-driver-e2e.mjs +55 -0
  390. package/scripts/_dbg-redesign-shots.mjs +34 -0
  391. package/scripts/_dbg-restart-persist.mjs +41 -0
  392. package/scripts/_dbg-rollback-mgr-audit.mjs +169 -0
  393. package/scripts/_dbg-rpg.mjs +71 -0
  394. package/scripts/_dbg-sc001-dispatch.mjs +63 -0
  395. package/scripts/_dbg-scenario-final-shot.mjs +24 -0
  396. package/scripts/_dbg-scenario-phase-e2.mjs +75 -0
  397. package/scripts/_dbg-scenario-phase-e3.mjs +96 -0
  398. package/scripts/_dbg-scenario-phase1.mjs +90 -0
  399. package/scripts/_dbg-scenario-phase2.mjs +105 -0
  400. package/scripts/_dbg-scenario-phase3-monitor.mjs +48 -0
  401. package/scripts/_dbg-scenario-phase3.mjs +111 -0
  402. package/scripts/_dbg-scene-polish-shot.mjs +118 -0
  403. package/scripts/_dbg-selring-probe.mjs +47 -0
  404. package/scripts/_dbg-sideinfo.mjs +101 -0
  405. package/scripts/_dbg-splitter.mjs +88 -0
  406. package/scripts/_dbg-team-repro.mjs +76 -0
  407. package/scripts/_dbg-town-alarm-e2e.mjs +40 -0
  408. package/scripts/_dbg-town-beauty-shots.mjs +54 -0
  409. package/scripts/_dbg-town-daq-shot.mjs +26 -0
  410. package/scripts/_dbg-town-full-e2e.mjs +104 -0
  411. package/scripts/_dbg-town-gate.mjs +44 -0
  412. package/scripts/_dbg-town-ins-probe.mjs +37 -0
  413. package/scripts/_dbg-town-perf.mjs +50 -0
  414. package/scripts/_dbg-town-polish-shots.mjs +82 -0
  415. package/scripts/_dbg-town-realtime.mjs +42 -0
  416. package/scripts/_dbg-town-shot3.mjs +17 -0
  417. package/scripts/_dbg-town-truth.mjs +37 -0
  418. package/scripts/_dbg-town-truth2.mjs +27 -0
  419. package/scripts/_dbg-transform-audit.mjs +98 -0
  420. package/scripts/_dbg-trend-expand-test.mjs +38 -0
  421. package/scripts/_dbg-twindaq-focus.mjs +81 -0
  422. package/scripts/_dbg-ui-polish-shot.mjs +43 -0
  423. package/scripts/_dbg-ui-polish-verify1.mjs +92 -0
  424. package/scripts/_dbg-ui-polish-verify2.mjs +50 -0
  425. package/scripts/_dbg-ui-polish-verify3.mjs +51 -0
  426. package/scripts/_dbg-ui-polish-verify4.mjs +50 -0
  427. package/scripts/_dbg-ui-shot.mjs +26 -0
  428. package/scripts/_dbg-unify-shot.mjs +43 -0
  429. package/scripts/_dbg-uplink-probe.mjs +36 -0
  430. package/scripts/_dbg-wizard-preview-shot.mjs +58 -0
  431. package/scripts/_dbg-wizard-ui-e2e.mjs +93 -0
  432. package/scripts/_dbg-ws-probe.mjs +86 -0
  433. package/scripts/_dbg-ws-shot2.mjs +23 -0
  434. package/scripts/_dbg-wsdot-sanity.mjs +25 -0
  435. package/scripts/_dev-stack.mjs +46 -0
  436. package/scripts/_http-driver-probe.mjs +30 -0
  437. package/scripts/_lib-dcw-line.mjs +24 -0
  438. package/scripts/_mqtt-driver-probe.mjs +30 -0
  439. package/scripts/_newdriver-rest-probe.mjs +100 -0
  440. package/scripts/_node-state-probe.mjs +5 -0
  441. package/scripts/_opcua-driver-probe.mjs +31 -0
  442. package/scripts/_rtu-debug.mjs +41 -0
  443. package/scripts/_rtu-direct.mjs +6 -0
  444. package/scripts/_rtu-driver-probe.mjs +53 -0
  445. package/scripts/_rtu-mini-slave.mjs +63 -0
  446. package/scripts/_townview_style.css +1185 -0
  447. package/scripts/_townview_template.html +1215 -0
  448. package/scripts/_wizard-shots.mjs +90 -0
  449. package/scripts/acceptance-suite.py +207 -0
  450. package/scripts/api-live-e2e.mjs +467 -0
  451. package/scripts/build-anime-models.mjs +339 -0
  452. package/scripts/build-character-models.mjs +175 -0
  453. package/scripts/build-film-devices.mjs +227 -0
  454. package/scripts/build-model-3d.mjs +173 -0
  455. package/scripts/build-player-sheet.mjs +58 -0
  456. package/scripts/build-world-map.mjs +188 -0
  457. package/scripts/build-wuwa-assets.mjs +345 -0
  458. package/scripts/collab-live-test.py +177 -0
  459. package/scripts/debug-mcp-http.ts +64 -0
  460. package/scripts/demo-lead-worker.ts +256 -0
  461. package/scripts/demo-modes.ts +415 -0
  462. package/scripts/demo-omp-channel.ts +135 -0
  463. package/scripts/design-preflight.mjs +223 -0
  464. package/scripts/design-verify-templates.mjs +110 -0
  465. package/scripts/design-verify.mjs +170 -0
  466. package/scripts/dev-guard.mjs +74 -0
  467. package/scripts/dev-modbus-simulator.mjs +55 -0
  468. package/scripts/dev-opcua-simulator.mjs +57 -0
  469. package/scripts/dev-protocol-simulators.mjs +145 -0
  470. package/scripts/dev-window.cmd +6 -0
  471. package/scripts/device-line-restore.mjs +34 -0
  472. package/scripts/diag-lead-status.ts +101 -0
  473. package/scripts/e2e-a2a-steer-live.mjs +193 -0
  474. package/scripts/e2e-agent-channel-task.ts +151 -0
  475. package/scripts/e2e-agent-team.mjs +168 -0
  476. package/scripts/e2e-auth-matrix.mjs +119 -0
  477. package/scripts/e2e-channel-isolation.ts +325 -0
  478. package/scripts/e2e-dual-drive-comm.ts +259 -0
  479. package/scripts/e2e-lead-team-mgmt.ts +217 -0
  480. package/scripts/e2e-memory-persistence.ts +320 -0
  481. package/scripts/e2e-memory-system.ts +332 -0
  482. package/scripts/e2e-omp-modes.ts +274 -0
  483. package/scripts/e2e-omp-realtime.ts +258 -0
  484. package/scripts/e2e-omp-workspace.ts +180 -0
  485. package/scripts/e2e-parallel-execution.mjs +151 -0
  486. package/scripts/e2e-rest-memory.mjs +251 -0
  487. package/scripts/e2e-rest-robustness.mjs +177 -0
  488. package/scripts/e2e-resume-crash.mjs +254 -0
  489. package/scripts/e2e-tab-close.mjs +136 -0
  490. package/scripts/e2e-task-queue.ts +277 -0
  491. package/scripts/e2e-template-isolation.mjs +267 -0
  492. package/scripts/e2e-token-ui.mjs +149 -0
  493. package/scripts/final-soup-run.py +192 -0
  494. package/scripts/final-soup-watch.py +83 -0
  495. package/scripts/home-bootstrap.mjs +117 -0
  496. package/scripts/i18n-codemod.mjs +173 -0
  497. package/scripts/i18n-fix-en.py +28 -0
  498. package/scripts/i18n-fix-en2.py +31 -0
  499. package/scripts/i18n-merge.mjs +67 -0
  500. package/scripts/omp-real-test.py +328 -0
  501. package/scripts/smoke-optimization-verify.ts +72 -0
  502. package/scripts/start.mjs +60 -0
  503. package/scripts/test-a2a-relay.mjs +222 -0
  504. package/scripts/test-a2a-types.ts +296 -0
  505. package/scripts/test-agent-brain.ts +135 -0
  506. package/scripts/test-agent-multi-channel.ts +160 -0
  507. package/scripts/test-agent-runtime.ts +348 -0
  508. package/scripts/test-api-client.ts +108 -0
  509. package/scripts/test-api-suite.mjs +271 -0
  510. package/scripts/test-block-consumption.mjs +194 -0
  511. package/scripts/test-channel-routing.ts +301 -0
  512. package/scripts/test-cmd-render.mjs +218 -0
  513. package/scripts/test-collab-e2e.mjs +136 -0
  514. package/scripts/test-collab-omp.mjs +192 -0
  515. package/scripts/test-dedup-logic.ts +132 -0
  516. package/scripts/test-dual-drive.ts +227 -0
  517. package/scripts/test-e2e-collaboration.ts +262 -0
  518. package/scripts/test-e2e-monitor.ts +192 -0
  519. package/scripts/test-event-history.ts +112 -0
  520. package/scripts/test-event-persistence.mjs +169 -0
  521. package/scripts/test-exec-modes.ts +316 -0
  522. package/scripts/test-fifo-realtime.mjs +193 -0
  523. package/scripts/test-final-e2e.mjs +290 -0
  524. package/scripts/test-full-system.ts +439 -0
  525. package/scripts/test-game-session.ts +160 -0
  526. package/scripts/test-goal-summary.ts +202 -0
  527. package/scripts/test-harness-liveness.ts +109 -0
  528. package/scripts/test-harness-terminal.ts +164 -0
  529. package/scripts/test-header-trail.mjs +95 -0
  530. package/scripts/test-hitl.ts +360 -0
  531. package/scripts/test-koda-restyle.mjs +105 -0
  532. package/scripts/test-lane-history-live.mjs +103 -0
  533. package/scripts/test-lane-raw.ts +67 -0
  534. package/scripts/test-lanes-history-render.mjs +138 -0
  535. package/scripts/test-loop-omp.mjs +154 -0
  536. package/scripts/test-mcp-tools.ts +285 -0
  537. package/scripts/test-mdlite.ts +53 -0
  538. package/scripts/test-memory-dynamic.ts +99 -0
  539. package/scripts/test-memory-maintenance.ts +117 -0
  540. package/scripts/test-memory-vector.ts +154 -0
  541. package/scripts/test-memory.ts +431 -0
  542. package/scripts/test-mock-collab.mjs +226 -0
  543. package/scripts/test-modern-render.mjs +174 -0
  544. package/scripts/test-omp-terminal.mjs +187 -0
  545. package/scripts/test-orchestration.ts +425 -0
  546. package/scripts/test-persistence-lazy.ts +323 -0
  547. package/scripts/test-prompt-system.mjs +181 -0
  548. package/scripts/test-prompts-loader.ts +56 -0
  549. package/scripts/test-protocol.ts +156 -0
  550. package/scripts/test-real-scenario.mjs +290 -0
  551. package/scripts/test-route-reason.mjs +111 -0
  552. package/scripts/test-scheduler-loop.ts +469 -0
  553. package/scripts/test-stream-render.ts +37 -0
  554. package/scripts/test-task-drawer.mjs +204 -0
  555. package/scripts/test-task-engine.ts +433 -0
  556. package/scripts/test-task-invariants.mjs +169 -0
  557. package/scripts/test-terminal-e2e.mjs +264 -0
  558. package/scripts/test-town-3d.mjs +121 -0
  559. package/scripts/test-town-drag.mjs +218 -0
  560. package/scripts/test-town-layout.mjs +200 -0
  561. package/scripts/test-town-page.mjs +136 -0
  562. package/scripts/test-town-scene-math.ts +169 -0
  563. package/scripts/test-town-twin.mjs +121 -0
  564. package/scripts/test-town-upload.mjs +89 -0
  565. package/scripts/test-town-user-flow.mjs +619 -0
  566. package/scripts/test-town-world.mjs +76 -0
  567. package/scripts/test-town-wuwa.mjs +198 -0
  568. package/scripts/test-user-isolation.mjs +193 -0
  569. package/scripts/test-workshop-db.ts +280 -0
  570. package/scripts/test-workshop-entries.ts +370 -0
  571. package/scripts/test-workspace-api.ts +329 -0
  572. package/scripts/test-ws-aep.mjs +187 -0
  573. package/scripts/test-ws-protocol.ts +134 -0
  574. package/scripts/test-ws-stress.mjs +208 -0
  575. package/scripts/town-demo-seed.mjs +75 -0
  576. package/scripts/town-scene-clear.mjs +56 -0
  577. package/scripts/ui-screenshot.mjs +128 -0
  578. package/scripts/verify-a2a-live.mjs +124 -0
  579. package/scripts/verify-exec-modes-report.ts +248 -0
  580. package/scripts/verify-game.mjs +477 -0
  581. package/scripts/verify-lead-mail.ts +188 -0
  582. package/scripts/verify-page-smoke.mjs +87 -0
  583. package/scripts/verify-ws-events.mjs +123 -0
  584. package/scripts/ws-test.mjs +176 -0
  585. package/scripts/ws-test.py +4 -0
  586. package/server/api/health.get.ts +32 -0
  587. package/server/api/mcp/workshop.ts +78 -0
  588. package/server/api/metrics.ts +75 -0
  589. package/server/api/system/config.get.ts +12 -0
  590. package/server/api/system/monitor/terminal/ws.ts +177 -0
  591. package/server/api/system/monitor/terminate.post.ts +36 -0
  592. package/server/api/system/monitor.get.ts +13 -0
  593. package/server/api/system/settings/events.get.ts +28 -0
  594. package/server/api/system/settings/index.get.ts +14 -0
  595. package/server/api/system/settings/index.patch.ts +22 -0
  596. package/server/api/system/settings/reset.post.ts +12 -0
  597. package/server/api/users/[id].delete.ts +11 -0
  598. package/server/api/users/[id].get.ts +11 -0
  599. package/server/api/users/[id].put.ts +14 -0
  600. package/server/api/users/index.get.ts +13 -0
  601. package/server/api/users/index.post.ts +13 -0
  602. package/server/api/users/login.post.ts +11 -0
  603. package/server/api/users/logout.post.ts +9 -0
  604. package/server/api/users/me.get.ts +9 -0
  605. package/server/api/users/register.post.ts +11 -0
  606. package/server/api/users/tokens/[id]/token.get.ts +11 -0
  607. package/server/api/users/tokens/[id].delete.ts +11 -0
  608. package/server/api/users/tokens/[id].patch.ts +14 -0
  609. package/server/api/users/tokens/index.get.ts +9 -0
  610. package/server/api/users/tokens/index.post.ts +13 -0
  611. package/server/api/workshop/[...path].ts +13 -0
  612. package/server/api/workshop/a2a/[agentId]/card/index.get.ts +75 -0
  613. package/server/api/workshop/a2a/[agentId]/rpc/index.post.ts +429 -0
  614. package/server/api/workshop/a2a/send.post.ts +28 -0
  615. package/server/api/workshop/agent-tools/approvals/[id]/decide.post.ts +21 -0
  616. package/server/api/workshop/agent-tools/approvals/index.get.ts +18 -0
  617. package/server/api/workshop/agent-tools/bindings/[id].delete.ts +21 -0
  618. package/server/api/workshop/agent-tools/bindings/[id].patch.ts +14 -0
  619. package/server/api/workshop/agent-tools/bindings/index.get.ts +14 -0
  620. package/server/api/workshop/agent-tools/bindings/index.post.ts +20 -0
  621. package/server/api/workshop/agent-tools/invoke.post.ts +34 -0
  622. package/server/api/workshop/agents/[id].delete.ts +20 -0
  623. package/server/api/workshop/agents/[id].get.ts +19 -0
  624. package/server/api/workshop/agents/[id].patch.ts +30 -0
  625. package/server/api/workshop/agents/index.get.ts +13 -0
  626. package/server/api/workshop/agents/index.post.ts +29 -0
  627. package/server/api/workshop/agents/subscribe.post.ts +21 -0
  628. package/server/api/workshop/approvals/[id]/decide.post.ts +30 -0
  629. package/server/api/workshop/approvals/index.get.ts +22 -0
  630. package/server/api/workshop/assets/character/[id].delete.ts +33 -0
  631. package/server/api/workshop/assets/character/index.get.ts +17 -0
  632. package/server/api/workshop/assets/character/index.post.ts +87 -0
  633. package/server/api/workshop/assets/devices/[id].delete.ts +43 -0
  634. package/server/api/workshop/assets/devices/index.get.ts +103 -0
  635. package/server/api/workshop/assets/devices/index.post.ts +53 -0
  636. package/server/api/workshop/audit.get.ts +43 -0
  637. package/server/api/workshop/caller.ts +141 -0
  638. package/server/api/workshop/channel-templates/[id]/instantiate.post.ts +31 -0
  639. package/server/api/workshop/channel-templates/[id].delete.ts +20 -0
  640. package/server/api/workshop/channel-templates/[id].get.ts +19 -0
  641. package/server/api/workshop/channel-templates/[id].patch.ts +48 -0
  642. package/server/api/workshop/channel-templates/from-channel.post.ts +31 -0
  643. package/server/api/workshop/channel-templates/index.get.ts +13 -0
  644. package/server/api/workshop/channel-templates/index.post.ts +61 -0
  645. package/server/api/workshop/channels/[id]/activate.post.ts +19 -0
  646. package/server/api/workshop/channels/[id]/agents/[agentId]/memories/[memoryId].delete.ts +30 -0
  647. package/server/api/workshop/channels/[id]/agents/[agentId]/memories/index.get.ts +29 -0
  648. package/server/api/workshop/channels/[id]/agents/[agentId]/memories/index.post.ts +48 -0
  649. package/server/api/workshop/channels/[id]/agents/[agentId]/memories/search.post.ts +43 -0
  650. package/server/api/workshop/channels/[id]/agents/[agentId]/model.patch.ts +39 -0
  651. package/server/api/workshop/channels/[id]/agents/[agentId]/position.patch.ts +33 -0
  652. package/server/api/workshop/channels/[id]/agents/[agentId]/range.patch.ts +54 -0
  653. package/server/api/workshop/channels/[id]/agents/[agentId]/stop.post.ts +25 -0
  654. package/server/api/workshop/channels/[id]/agents/[agentId].delete.ts +23 -0
  655. package/server/api/workshop/channels/[id]/agents/[agentId].patch.ts +28 -0
  656. package/server/api/workshop/channels/[id]/agents/index.get.ts +27 -0
  657. package/server/api/workshop/channels/[id]/agents/index.post.ts +61 -0
  658. package/server/api/workshop/channels/[id]/events.get.ts +55 -0
  659. package/server/api/workshop/channels/[id]/index.get.ts +18 -0
  660. package/server/api/workshop/channels/[id]/index.patch.ts +30 -0
  661. package/server/api/workshop/channels/[id]/memories/[memoryId].delete.ts +29 -0
  662. package/server/api/workshop/channels/[id]/memories/index.get.ts +28 -0
  663. package/server/api/workshop/channels/[id]/memories/index.post.ts +42 -0
  664. package/server/api/workshop/channels/[id]/messages/index.get.ts +56 -0
  665. package/server/api/workshop/channels/[id]/messages/index.post.ts +75 -0
  666. package/server/api/workshop/channels/[id]/queue.get.ts +25 -0
  667. package/server/api/workshop/channels/[id]/tasks/index.get.ts +24 -0
  668. package/server/api/workshop/channels/[id]/tasks/index.post.ts +79 -0
  669. package/server/api/workshop/channels/[id]/terminals.get.ts +21 -0
  670. package/server/api/workshop/channels/[id].delete.ts +18 -0
  671. package/server/api/workshop/channels.get.ts +11 -0
  672. package/server/api/workshop/channels.post.ts +48 -0
  673. package/server/api/workshop/daq/[id]/bind.post.ts +19 -0
  674. package/server/api/workshop/daq/[id]/samples.get.ts +22 -0
  675. package/server/api/workshop/daq/[id]/test.post.ts +17 -0
  676. package/server/api/workshop/daq/[id].delete.ts +30 -0
  677. package/server/api/workshop/daq/[id].patch.ts +31 -0
  678. package/server/api/workshop/daq/alarms/[id]/ack.post.ts +15 -0
  679. package/server/api/workshop/daq/alarms.get.ts +16 -0
  680. package/server/api/workshop/daq/controller.post.ts +52 -0
  681. package/server/api/workshop/daq/index.get.ts +40 -0
  682. package/server/api/workshop/daq/index.post.ts +33 -0
  683. package/server/api/workshop/daq/infra/reconnect.post.ts +17 -0
  684. package/server/api/workshop/daq/infra.get.ts +11 -0
  685. package/server/api/workshop/daq/templates/[key].delete.ts +17 -0
  686. package/server/api/workshop/daq/templates/[key].patch.ts +18 -0
  687. package/server/api/workshop/daq/templates/index.post.ts +17 -0
  688. package/server/api/workshop/daq/test-driver.post.ts +23 -0
  689. package/server/api/workshop/dcw/[id]/bind.post.ts +17 -0
  690. package/server/api/workshop/dcw/[id]/param-ledger.get.ts +15 -0
  691. package/server/api/workshop/dcw/[id]/test.post.ts +15 -0
  692. package/server/api/workshop/dcw/[id]/write.post.ts +19 -0
  693. package/server/api/workshop/dcw/[id].delete.ts +30 -0
  694. package/server/api/workshop/dcw/[id].patch.ts +18 -0
  695. package/server/api/workshop/dcw/controller.post.ts +43 -0
  696. package/server/api/workshop/dcw/index.get.ts +25 -0
  697. package/server/api/workshop/dcw/index.post.ts +20 -0
  698. package/server/api/workshop/dcw/journal/index.get.ts +21 -0
  699. package/server/api/workshop/dcw/journal/node/[nodeId]/rollback.post.ts +22 -0
  700. package/server/api/workshop/dcw/line/query.get.ts +31 -0
  701. package/server/api/workshop/dcw/lines/[id]/start.post.ts +35 -0
  702. package/server/api/workshop/dcw/lines/[id]/stop.post.ts +34 -0
  703. package/server/api/workshop/dcw/lines/[id].delete.ts +35 -0
  704. package/server/api/workshop/dcw/lines/[id].patch.ts +15 -0
  705. package/server/api/workshop/dcw/lines/index.get.ts +14 -0
  706. package/server/api/workshop/dcw/lines/index.post.ts +31 -0
  707. package/server/api/workshop/dcw/optimizations/[id]/judge.post.ts +24 -0
  708. package/server/api/workshop/dcw/optimizations/[id]/rollback.post.ts +29 -0
  709. package/server/api/workshop/dcw/optimizations/[id]/series.get.ts +18 -0
  710. package/server/api/workshop/dcw/optimizations/[id].get.ts +17 -0
  711. package/server/api/workshop/dcw/optimizations/index.get.ts +23 -0
  712. package/server/api/workshop/dcw/products/[id].delete.ts +16 -0
  713. package/server/api/workshop/dcw/products/[id].patch.ts +17 -0
  714. package/server/api/workshop/dcw/products/index.post.ts +16 -0
  715. package/server/api/workshop/dcw/recipes/[id]/apply.post.ts +40 -0
  716. package/server/api/workshop/dcw/recipes/[id]/mark-good.post.ts +21 -0
  717. package/server/api/workshop/dcw/recipes/[id]/rollback-good.post.ts +19 -0
  718. package/server/api/workshop/dcw/recipes/[id].delete.ts +14 -0
  719. package/server/api/workshop/dcw/recipes/[id].patch.ts +16 -0
  720. package/server/api/workshop/dcw/recipes/index.get.ts +12 -0
  721. package/server/api/workshop/dcw/recipes/index.post.ts +15 -0
  722. package/server/api/workshop/dcw/runs/[id]/close.post.ts +15 -0
  723. package/server/api/workshop/dcw/runs/[id]/data.get.ts +15 -0
  724. package/server/api/workshop/dcw/runs/[id]/rollback.post.ts +20 -0
  725. package/server/api/workshop/dcw/runs/index.get.ts +11 -0
  726. package/server/api/workshop/dcw/templates/[key].delete.ts +14 -0
  727. package/server/api/workshop/dcw/templates/[key].patch.ts +16 -0
  728. package/server/api/workshop/dcw/templates/index.post.ts +15 -0
  729. package/server/api/workshop/dcw/test-driver.post.ts +16 -0
  730. package/server/api/workshop/device-twins/[id]/control.post.ts +21 -0
  731. package/server/api/workshop/device-twins/[id]/telemetry.post.ts +21 -0
  732. package/server/api/workshop/device-twins/[id].delete.ts +25 -0
  733. package/server/api/workshop/device-twins/[id].patch.ts +29 -0
  734. package/server/api/workshop/device-twins/index.get.ts +16 -0
  735. package/server/api/workshop/device-twins/index.post.ts +32 -0
  736. package/server/api/workshop/fs/dirs.get.ts +40 -0
  737. package/server/api/workshop/mailbox/all.get.ts +19 -0
  738. package/server/api/workshop/mailbox.get.ts +14 -0
  739. package/server/api/workshop/memories/maintenance.post.ts +15 -0
  740. package/server/api/workshop/ops-logs/index.get.ts +30 -0
  741. package/server/api/workshop/ops-logs/index.post.ts +39 -0
  742. package/server/api/workshop/runtime.get.ts +11 -0
  743. package/server/api/workshop/scene/layouts/[channelId].delete.ts +23 -0
  744. package/server/api/workshop/scene/layouts/[channelId].put.ts +31 -0
  745. package/server/api/workshop/scene/layouts.get.ts +13 -0
  746. package/server/api/workshop/tasks/[id]/cancel/index.post.ts +34 -0
  747. package/server/api/workshop/tasks/[id]/complete.post.ts +32 -0
  748. package/server/api/workshop/tasks/[id]/dispatch.post.ts +30 -0
  749. package/server/api/workshop/tasks/[id]/report.post.ts +38 -0
  750. package/server/api/workshop/tasks/[id]/retry.post.ts +33 -0
  751. package/server/api/workshop/tasks/[id].get.ts +32 -0
  752. package/server/api/workshop/tasks.get.ts +13 -0
  753. package/server/api/workshop/teams/[id]/deploy.post.ts +40 -0
  754. package/server/api/workshop/teams/[id]/members/[agentId].delete.ts +19 -0
  755. package/server/api/workshop/teams/[id]/members/index.get.ts +19 -0
  756. package/server/api/workshop/teams/[id]/members/index.post.ts +32 -0
  757. package/server/api/workshop/teams/[id].delete.ts +20 -0
  758. package/server/api/workshop/teams/[id].get.ts +19 -0
  759. package/server/api/workshop/teams/[id].patch.ts +32 -0
  760. package/server/api/workshop/teams/index.get.ts +13 -0
  761. package/server/api/workshop/teams/index.post.ts +27 -0
  762. package/server/api/workshop/users/me.get.ts +23 -0
  763. package/server/api/workshop/users/register.post.ts +33 -0
  764. package/server/api/workshop/workspaces/[id]/channel-templates/[tplId].post.ts +31 -0
  765. package/server/api/workshop/workspaces/[id]/channels/[channelId].delete.ts +15 -0
  766. package/server/api/workshop/workspaces/[id]/channels/[channelId].post.ts +15 -0
  767. package/server/api/workshop/workspaces/[id]/index.delete.ts +14 -0
  768. package/server/api/workshop/workspaces/index.get.ts +11 -0
  769. package/server/api/workshop/workspaces/index.post.ts +17 -0
  770. package/server/api/workshop/ws.ts +694 -0
  771. package/server/data/agent-node-bindings.json +554 -0
  772. package/server/data/daq-templates.json +16 -0
  773. package/server/data/daqs.json +2680 -0
  774. package/server/data/dcw-lines.json +919 -0
  775. package/server/data/dcw-products.json +835 -0
  776. package/server/data/dcw-recipes.json +1650 -0
  777. package/server/data/dcw-rollback.json +9339 -0
  778. package/server/data/dcw-runs.json +4633 -0
  779. package/server/data/dcw-templates.json +26 -0
  780. package/server/data/dcw-writes.json +9770 -0
  781. package/server/data/dcws.json +1736 -0
  782. package/server/data/device-twins.json +133 -0
  783. package/server/data/line-runs.json +152 -0
  784. package/server/data/scene-layouts.json +200 -0
  785. package/server/mcp/workshop-server.ts +510 -0
  786. package/server/middleware/request-log.ts +15 -0
  787. package/server/plugins/backup.ts +85 -0
  788. package/server/plugins/daq.ts +65 -0
  789. package/server/plugins/dev-stability-guard.ts +42 -0
  790. package/server/plugins/security.ts +29 -0
  791. package/server/plugins/system-config.ts +38 -0
  792. package/server/plugins/workshop.ts +145 -0
  793. package/server/repositories/user.repository.ts +391 -0
  794. package/server/schemas/user.schema.ts +67 -0
  795. package/server/services/system-config.ts +356 -0
  796. package/server/services/user.service.ts +186 -0
  797. package/server/services/workshop/agents/adapters/omp-rpc-client.ts +539 -0
  798. package/server/services/workshop/agents/adapters/to-a2a.ts +40 -0
  799. package/server/services/workshop/agents/agent-interface.ts +219 -0
  800. package/server/services/workshop/agents/claude-agent.ts +47 -0
  801. package/server/services/workshop/agents/factory.ts +24 -0
  802. package/server/services/workshop/agents/harness-process.ts +158 -0
  803. package/server/services/workshop/agents/harness-terminal.ts +545 -0
  804. package/server/services/workshop/agents/industrial-context.ts +222 -0
  805. package/server/services/workshop/agents/industrial-tools.ts +362 -0
  806. package/server/services/workshop/agents/mock-agent.ts +396 -0
  807. package/server/services/workshop/agents/node-bindings.repo.ts +165 -0
  808. package/server/services/workshop/agents/omp-agent.ts +1598 -0
  809. package/server/services/workshop/agents/tool-approvals.ts +174 -0
  810. package/server/services/workshop/assets/character-asset.repo.ts +120 -0
  811. package/server/services/workshop/assets/device-twin.repo.ts +236 -0
  812. package/server/services/workshop/daq/alarm-notify.ts +100 -0
  813. package/server/services/workshop/daq/bus/index.ts +46 -0
  814. package/server/services/workshop/daq/bus/inproc.adapter.ts +62 -0
  815. package/server/services/workshop/daq/bus/mqtt.adapter.ts +112 -0
  816. package/server/services/workshop/daq/bus/queue-port.ts +36 -0
  817. package/server/services/workshop/daq/daq-controller.ts +778 -0
  818. package/server/services/workshop/daq/daq-node.repo.ts +96 -0
  819. package/server/services/workshop/daq/daq-node.ts +247 -0
  820. package/server/services/workshop/daq/daq-runtime.ts +168 -0
  821. package/server/services/workshop/daq/daq-templates.ts +157 -0
  822. package/server/services/workshop/daq/drivers.ts +771 -0
  823. package/server/services/workshop/daq/host-bindings.ts +71 -0
  824. package/server/services/workshop/daq/host-ports.ts +64 -0
  825. package/server/services/workshop/daq/infra.ts +202 -0
  826. package/server/services/workshop/daq/storage/index.ts +96 -0
  827. package/server/services/workshop/daq/storage/sqlite.adapter.ts +205 -0
  828. package/server/services/workshop/daq/storage/timescale.adapter.ts +185 -0
  829. package/server/services/workshop/daq/storage/tsdb-port.ts +74 -0
  830. package/server/services/workshop/db/agent.repo.ts +105 -0
  831. package/server/services/workshop/db/channel-agent.repo.ts +119 -0
  832. package/server/services/workshop/db/channel-event.repo.ts +154 -0
  833. package/server/services/workshop/db/channel-template.repo.ts +113 -0
  834. package/server/services/workshop/db/channel.repo.ts +101 -0
  835. package/server/services/workshop/db/database.ts +934 -0
  836. package/server/services/workshop/db/memory.repo.ts +249 -0
  837. package/server/services/workshop/db/message.repo.ts +149 -0
  838. package/server/services/workshop/db/ops.repo.ts +323 -0
  839. package/server/services/workshop/db/schema.sql +143 -0
  840. package/server/services/workshop/db/subscription.repo.ts +62 -0
  841. package/server/services/workshop/db/task.repo.ts +175 -0
  842. package/server/services/workshop/db/team-member.repo.ts +70 -0
  843. package/server/services/workshop/db/team.repo.ts +109 -0
  844. package/server/services/workshop/db/user.repo.ts +100 -0
  845. package/server/services/workshop/dcw/dcw-controller.ts +841 -0
  846. package/server/services/workshop/dcw/dcw-line.repo.ts +86 -0
  847. package/server/services/workshop/dcw/dcw-node.repo.ts +89 -0
  848. package/server/services/workshop/dcw/dcw-node.ts +200 -0
  849. package/server/services/workshop/dcw/dcw-product.repo.ts +95 -0
  850. package/server/services/workshop/dcw/dcw-recipe.repo.ts +349 -0
  851. package/server/services/workshop/dcw/dcw-runtime.ts +82 -0
  852. package/server/services/workshop/dcw/dcw-templates.ts +138 -0
  853. package/server/services/workshop/dcw/drivers.ts +523 -0
  854. package/server/services/workshop/dcw/line-run.ts +101 -0
  855. package/server/services/workshop/dcw/recipe-rollback-manager.ts +702 -0
  856. package/server/services/workshop/dcw/recipe-rollback.repo.ts +197 -0
  857. package/server/services/workshop/logger.ts +58 -0
  858. package/server/services/workshop/ops/ops.ts +78 -0
  859. package/server/services/workshop/prompts/loader.ts +112 -0
  860. package/server/services/workshop/runtime/agent-runtime.ts +698 -0
  861. package/server/services/workshop/runtime/channel-runtime.ts +202 -0
  862. package/server/services/workshop/runtime/embedding-provider.ts +79 -0
  863. package/server/services/workshop/runtime/execution-mode.ts +215 -0
  864. package/server/services/workshop/runtime/mailbox.ts +148 -0
  865. package/server/services/workshop/runtime/manager.ts +2543 -0
  866. package/server/services/workshop/runtime/memory.ts +439 -0
  867. package/server/services/workshop/runtime/monitor.ts +194 -0
  868. package/server/services/workshop/runtime/scheduler-loop.ts +773 -0
  869. package/server/services/workshop/runtime/task-engine.ts +574 -0
  870. package/server/services/workshop/scene/scene-layout.repo.ts +112 -0
  871. package/server/services/workshop/scene-events.ts +63 -0
  872. package/server/services/workshop/types/a2a.ts +61 -0
  873. package/server/services/workshop/types/task.ts +85 -0
  874. package/server/types/user.ts +53 -0
  875. package/server/utils/approval-gate.ts +67 -0
  876. package/server/utils/auth.ts +13 -0
  877. package/server/utils/config.ts +40 -0
  878. package/server/utils/errors.ts +24 -0
  879. package/server/utils/response.ts +49 -0
  880. package/server/utils/validate.ts +19 -0
  881. package/shared/config/engine.d.mts +41 -0
  882. package/shared/config/engine.mjs +291 -0
  883. package/shared/config/home.d.mts +48 -0
  884. package/shared/config/home.mjs +146 -0
  885. package/shared/config/schema.json +30 -0
  886. package/shared/daq-protocol.ts +361 -0
  887. package/shared/dcw-protocol.ts +576 -0
  888. package/shared/terminal-protocol.ts +70 -0
  889. package/shared/town-anim.ts +56 -0
  890. package/shared/town-behavior.ts +136 -0
  891. package/shared/town-protocol.ts +110 -0
  892. package/shared/town-scene-math.ts +283 -0
  893. package/shared/workshop-protocol.ts +199 -0
  894. package/tsconfig.json +10 -0
  895. package/uno.config.ts +37 -0
@@ -0,0 +1,2543 @@
1
+ /**
2
+ * AgentChannelManager — 统一管理(对象化)。
3
+ * Channel / Agent 模板 / Channel Agent 实例 全是对象;所有作业方法做权限校验(lead/assignee/channel 作用域)。
4
+ *
5
+ * v3:Agent 模板与 Channel 实例分离:
6
+ * - Agent 模板(agents):全局可复用数据结构(name/harness/config/enabled)
7
+ * - Channel 实例(channel_agents):每次把模板放入 channel 都克隆出独立身份 id 的新实例
8
+ * (name/harness/config 复制自模板,另含独立 role + token)
9
+ * - 每个实例在各自 channel 独立装配 AgentRuntime(独立 mailbox/impl 子进程/状态)
10
+ * 权威契约见 docs/superpowers/plans/2026-08-13-agent-workshop-multi-agent.md 核心契约块 T3。
11
+ */
12
+ import { createLogger } from '../logger'
13
+ import { mkdirSync } from 'node:fs'
14
+ import { resolve } from 'node:path'
15
+ import { randomUUID } from 'node:crypto'
16
+ import type { DatabaseSync } from 'node:sqlite'
17
+ import { AppError } from '../../../utils/errors'
18
+ import { userRepository } from '../../../repositories/user.repository'
19
+ import type { A2AMessage, A2AArtifact, ChannelMail, Part } from '../types/a2a'
20
+ import type { AgentStatusView, AgentTaskQueueView, TaskState, WorkspaceTask } from '../types/task'
21
+ import { TERMINAL_TASK_STATES } from '../types/task'
22
+ import type { AgentInfo, AgentInterface, AgentWorkspace, AgentEvent, ExecutionMode } from '../agents/agent-interface'
23
+ import type { ModeConfig } from './execution-mode'
24
+ import { encodeTaskMode, isGoalSummaryArtifact } from './execution-mode'
25
+ import type { ChannelRepo } from '../db/channel.repo'
26
+ import type { AgentRepo } from '../db/agent.repo'
27
+ import type { TeamRepo } from '../db/team.repo'
28
+ import type { TeamMemberRepo } from '../db/team-member.repo'
29
+ import type { ChannelAgentRepo } from '../db/channel-agent.repo'
30
+ import type { MessageRepo } from '../db/message.repo'
31
+ import type { SubscriptionRepo } from '../db/subscription.repo'
32
+ import type { TaskRepo, TaskPatch } from '../db/task.repo'
33
+ import { parseJson, type AgentRow, type ChannelAgentRow, type ChannelRow, type ChannelTemplateRow, type MemoryRow, type MessageRow, type TaskRow, type TeamRow, type UserRow, type WorkspaceRow } from '../db/database'
34
+ import { Mailbox, rowToMessage } from './mailbox'
35
+ import { AgentRuntime } from './agent-runtime'
36
+ import type { ChannelBus, MemberChangeEvent, TaskEngine, TaskEventTask } from './agent-runtime'
37
+ import { ChannelRuntime } from './channel-runtime'
38
+ import { SchedulerLoop, type SchedulerLoopOptions } from './scheduler-loop'
39
+ import { TaskEngine as TaskEngineImpl } from './task-engine'
40
+ import { AgentMemory, envNum, runMemoryMaintenance, segmentCJK, unsegmentCJK, vectorizeMemory, type MaintenanceResult, type MemorySnippet } from './memory'
41
+ import { createEnvEmbeddingProvider } from './embedding-provider'
42
+ import { listHarnessProcesses, listAliveHarnessProcessesByAgent, sweepHarnessProcesses, killHarnessProcess } from '../agents/harness-process'
43
+ import { hasTerminalSession, sweepTerminalSessions } from '../agents/harness-terminal'
44
+ import { TEAM_AGENT_ID, type MemoryRepo } from '../db/memory.repo'
45
+ import type { UserRepo } from '../db/user.repo'
46
+ import type { ChannelEventRepo } from '../db/channel-event.repo'
47
+ import type { ChannelTemplateRepo, ChannelTemplateMember } from '../db/channel-template.repo'
48
+
49
+ const log = createLogger('workshop.manager')
50
+
51
+ /** 全部仓储(依赖注入) */
52
+ export interface AllRepos {
53
+ users: UserRepo
54
+ channelEvents: ChannelEventRepo
55
+ channels: ChannelRepo
56
+ agents: AgentRepo
57
+ teams: TeamRepo
58
+ teamMembers: TeamMemberRepo
59
+ channelTemplates: ChannelTemplateRepo
60
+ channelAgents: ChannelAgentRepo
61
+ messages: MessageRepo
62
+ subscriptions: SubscriptionRepo
63
+ tasks: TaskRepo
64
+ memories: MemoryRepo
65
+ }
66
+
67
+ /** Manager 依赖 */
68
+ export interface ManagerDeps {
69
+ repos: AllRepos
70
+ implFactory: (agent: AgentInfo) => AgentInterface
71
+ taskEngineFactory?: (repos: { tasks: TaskRepo, messages: MessageRepo }) => TaskEngine
72
+ db: DatabaseSync
73
+ }
74
+
75
+ /** 权限主体(路由层 ResolvedUser 的子集;role 供 admin 判定) */
76
+ export interface ActingUser {
77
+ id: string
78
+ role?: string
79
+ }
80
+
81
+ /** Agent 模板详情(全局;含其克隆出的全部实例) */
82
+ export interface AgentTemplateDetail {
83
+ id: string
84
+ name: string
85
+ harness: string
86
+ config: Record<string, unknown>
87
+ enabled: number
88
+ /** 可见性:'private' | 'public' */
89
+ visibility: string
90
+ /** 内置模板(owner NULL;公开只读,任何人含 admin 不可修改删除) */
91
+ isBuiltin: boolean
92
+ /** 归属用户(null = 内置公共) */
93
+ ownerUserId: string | null
94
+ /** 该模板克隆出的全部实例(跨 channel) */
95
+ instances: Array<{ id: string, channelId: string, role: 'lead' | 'worker', token: string }>
96
+ createdAt: string
97
+ updatedAt: string
98
+ }
99
+
100
+ /** AgentTeam 详情(含成员模板快照) */
101
+ export interface AgentTeamDetail {
102
+ id: string
103
+ name: string
104
+ description: string
105
+ /** 可见性:'private' | 'public' */
106
+ visibility: string
107
+ /** 内置编组(owner NULL;公开只读,任何人含 admin 不可修改删除) */
108
+ isBuiltin: boolean
109
+ /** 归属用户(null = 内置公共) */
110
+ ownerUserId: string | null
111
+ /** 成员(按加入顺序;快照含模板当前 name/harness,便于前端展示) */
112
+ members: Array<{
113
+ templateId: string
114
+ name: string
115
+ harness: string
116
+ role: 'lead' | 'worker'
117
+ addedAt: string
118
+ }>
119
+ createdAt: string
120
+ updatedAt: string
121
+ }
122
+
123
+ /** Channel 模板详情(场景 + 工作目录 + 成员组合;lead/members 已反序列化) */
124
+ export interface ChannelTemplateDetail {
125
+ id: string
126
+ name: string
127
+ description: string
128
+ scenarioPrompt: string
129
+ workspace: string
130
+ /** 内联 lead 定义(空 = 无 lead) */
131
+ lead: { name: string, harness: string, config?: Record<string, unknown> } | null
132
+ /** 成员组合:引用 Agent 模板或内联定义 */
133
+ members: ChannelTemplateMember[]
134
+ visibility: string
135
+ isBuiltin: boolean
136
+ ownerUserId: string | null
137
+ createdAt: string
138
+ updatedAt: string
139
+ }
140
+
141
+ /** ChannelAgentRow → AgentInfo(实例视图:实例 id + 本 channel 的 role/token + 复制的 name/harness/config) */
142
+ function instanceToAgentInfo(m: ChannelAgentRow): AgentInfo {
143
+ return {
144
+ id: m.id,
145
+ channelId: m.channelId,
146
+ name: m.name,
147
+ harness: m.harness,
148
+ role: m.role as 'lead' | 'worker',
149
+ config: parseJson<Record<string, unknown>>(m.configJson, {}),
150
+ token: m.token,
151
+ enabled: m.enabled,
152
+ }
153
+ }
154
+
155
+ /** TaskRow → WorkspaceTask(artifacts/history 反序列化) */
156
+ function rowToTask(row: TaskRow): WorkspaceTask {
157
+ return {
158
+ id: row.id,
159
+ channelId: row.channelId,
160
+ parentId: row.parentId ?? undefined,
161
+ assigneeId: row.assigneeId,
162
+ creatorId: row.creatorId ?? '',
163
+ title: row.title,
164
+ description: row.description ?? undefined,
165
+ state: row.state as TaskState,
166
+ progress: row.progress,
167
+ retryCount: row.retryCount,
168
+ artifacts: parseJson<A2AArtifact[]>(row.artifactsJson, []),
169
+ history: parseJson<A2AMessage[]>(row.historyJson, []),
170
+ routeReason: row.routeReason || undefined,
171
+ createdAt: row.createdAt,
172
+ updatedAt: row.updatedAt,
173
+ }
174
+ }
175
+
176
+ /** MessageRow → ChannelMail(parts/metadata 反序列化;渠道邮件公开投影) */
177
+ function rowToChannelMail(row: MessageRow): ChannelMail {
178
+ return {
179
+ messageId: row.id,
180
+ taskId: row.taskId,
181
+ fromAgentId: row.fromAgentId,
182
+ toAgentId: row.toAgentId,
183
+ role: row.role as 'ROLE_USER' | 'ROLE_AGENT',
184
+ parts: parseJson<Part[]>(row.partsJson, []),
185
+ metadata: parseJson<Record<string, unknown>>(row.metadataJson, {}),
186
+ state: row.state,
187
+ createdAt: row.createdAt,
188
+ consumedAt: row.consumedAt,
189
+ }
190
+ }
191
+
192
+ /** 构造 A2AMessage(统一入口) */
193
+ function buildMessage(
194
+ channelId: string,
195
+ role: 'ROLE_USER' | 'ROLE_AGENT',
196
+ parts: Part[],
197
+ metadata: Record<string, unknown>,
198
+ ): A2AMessage {
199
+ return { messageId: randomUUID(), contextId: channelId, role, parts, metadata }
200
+ }
201
+
202
+ /** 运行时实例复合键(channel, 实例) */
203
+ function runtimeKey(channelId: string, agentId: string): string {
204
+ return `${channelId}\u0000${agentId}`
205
+ }
206
+
207
+ /** factory 支持的 harness 集(lead 建成员时校验;与 agents/factory.ts 对齐) */
208
+ const KNOWN_HARNESSES = new Set(['mock', 'omp', 'claude'])
209
+
210
+ /** 全局用户名解析(带 60s 缓存;owner 归属呈现用,解析失败返回 null) */
211
+ const ownerNameCache = new Map<string, { name: string | null, at: number }>()
212
+ export function resolveOwnerName(userId: string): string | null {
213
+ const hit = ownerNameCache.get(userId)
214
+ if (hit && Date.now() - hit.at < 60_000) return hit.name
215
+ let name: string | null
216
+ try {
217
+ name = userRepository.findById(userId)?.name ?? null
218
+ }
219
+ catch {
220
+ name = null
221
+ }
222
+ ownerNameCache.set(userId, { name, at: Date.now() })
223
+ return name
224
+ }
225
+
226
+ /** 运行时资源监控:单个 ChannelRuntime 视图 */
227
+ export interface RuntimeChannelView {
228
+ channelId: string
229
+ /** 已装配(wired)的 AgentRuntime 数 */
230
+ wiredAgentCount: number
231
+ /** channel 内成员总数(含未装配;来自 DB) */
232
+ memberCount: number
233
+ /** 是否为 lead 装配并启动了 SchedulerLoop */
234
+ hasScheduler: boolean
235
+ leadAgentId: string | null
236
+ /** 归属用户(admin 全量视图附带;普通用户视图为自身) */
237
+ ownerUserId?: string | null
238
+ /** 归属用户名(admin 视角呈现创建者) */
239
+ ownerName?: string | null
240
+ }
241
+
242
+ /** 运行时资源监控:单个 AgentRuntime 视图 */
243
+ export interface RuntimeAgentView {
244
+ channelId: string
245
+ agentId: string
246
+ name: string
247
+ role: 'lead' | 'worker'
248
+ harness: string
249
+ state: 'idle' | 'busy' | 'stopped'
250
+ currentTaskId: string | null
251
+ queuedCount: number
252
+ completedCount: number
253
+ /** harness 进程(进程内 harness 为 null) */
254
+ process: { pid: number, alive: boolean, command: string } | null
255
+ /** 归属用户(admin 全量视图附带;普通用户视图为自身) */
256
+ ownerUserId?: string | null
257
+ ownerName?: string | null
258
+ }
259
+
260
+ /** 运行时资源监控:harnest 进程视图(注册表,含孤儿) */
261
+ export interface RuntimeProcessView {
262
+ pid: number
263
+ harness: string
264
+ command: string
265
+ args: string[]
266
+ agentId: string | null
267
+ channelId: string | null
268
+ name: string | null
269
+ role: 'lead' | 'worker' | null
270
+ startedAt: number
271
+ alive: boolean
272
+ exitCode: number | null
273
+ /** 是否被某个已装配 runtime 引用(否则为孤儿进程) */
274
+ bound: boolean
275
+ /** 终端镜像是否可接入(harness-terminal tap 已挂载;/monitor 终端按钮依据) */
276
+ terminal: boolean
277
+ }
278
+
279
+ /** 运行时资源监控:全量快照 */
280
+ export interface RuntimeMonitorSnapshot {
281
+ generatedAt: string
282
+ /** 服务端(宿主)进程 pid */
283
+ serverPid: number
284
+ uptimeMs: number
285
+ channels: RuntimeChannelView[]
286
+ agents: RuntimeAgentView[]
287
+ processes: RuntimeProcessView[]
288
+ counts: {
289
+ channels: number
290
+ agents: number
291
+ processes: number
292
+ aliveProcesses: number
293
+ orphanProcesses: number
294
+ }
295
+ }
296
+
297
+ export class AgentChannelManager {
298
+ private channels = new Map<string, ChannelRuntime>()
299
+ /** 键 = runtimeKey(channelId, 实例 id);每个实例一个独立运行时 */
300
+ private agentIndex = new Map<string, AgentRuntime>()
301
+ private buses = new Map<string, ChannelBus>()
302
+ private taskEngine: TaskEngine | null = null
303
+ private idleSweeperTimer: NodeJS.Timeout | null = null
304
+ private memoryTimer: NodeJS.Timeout | null = null
305
+ /** 全 manager 共享的 env 向量 provider(未配置 → null 纯 FTS;熔断/维度全体实例共享) */
306
+ private readonly memoryEmbedder = createEnvEmbeddingProvider()
307
+
308
+ constructor(private deps: ManagerDeps) {
309
+ // 记忆衰减清理定时器(失败只记日志,绝不抛出;unref 不阻进程退出;非法/非正 env 回退默认)
310
+ this.memoryTimer = setInterval(() => {
311
+ try {
312
+ runMemoryMaintenance(this.deps.repos.memories)
313
+ }
314
+ catch (err) {
315
+ log.error('[memory] 维护任务异常', err)
316
+ }
317
+ }, envNum('AW_MEMORY_MAINTENANCE_MS', 6 * 3600_000))
318
+ this.memoryTimer.unref?.()
319
+ }
320
+
321
+ // ===== 运行时装配 =====
322
+
323
+ private getTaskEngine(): TaskEngine {
324
+ if (!this.taskEngine) {
325
+ const factory = this.deps.taskEngineFactory ?? (r => new TaskEngineImpl(r, {
326
+ onTaskChange: (e) => {
327
+ // 全字段转发(含 progress):TaskEngine 内部进度变化(applyEvent 分块折算 /
328
+ // complete 置 100)须经总线 → WS task.progress 实时同步,否则前端实体进度滞后
329
+ this.buses.get(e.channelId)?.notifyTask({
330
+ taskId: e.taskId,
331
+ state: e.state,
332
+ progress: e.progress,
333
+ agentId: e.agentId,
334
+ task: e.task,
335
+ })
336
+ // 事件驱动调度:任务状态变化即唤醒该频道调度循环(空闲退避即刻恢复快节奏)
337
+ this.channels.get(e.channelId)?.scheduler?.wake()
338
+ },
339
+ }))
340
+ this.taskEngine = factory({ tasks: this.deps.repos.tasks, messages: this.deps.repos.messages })
341
+ }
342
+ return this.taskEngine
343
+ }
344
+
345
+ private ensureChannelRuntime(channelId: string): ChannelRuntime {
346
+ let cr = this.channels.get(channelId)
347
+ if (!cr) {
348
+ cr = new ChannelRuntime(channelId, {
349
+ taskEngine: this.getTaskEngine(),
350
+ subscriptionRepo: this.deps.repos.subscriptions,
351
+ channelAgents: this.deps.repos.channelAgents,
352
+ // 路由成功 → 总线通知(a2a.message 帧):经 runtime 回调统一收口,
353
+ // 调度器直呼 channelRuntime.route 的消息同样可见
354
+ }, msg => this.buses.get(channelId)?.notifyMessage(msg))
355
+ cr.setLoader(id => this.ensureAgentRuntime(channelId, id))
356
+ this.channels.set(channelId, cr)
357
+ this.buses.set(channelId, this.buildBus(cr))
358
+ }
359
+ return cr
360
+ }
361
+
362
+ private buildBus(cr: ChannelRuntime): ChannelBus {
363
+ const eventListeners = new Set<(event: AgentEvent, source: A2AMessage) => void>()
364
+ const taskListeners = new Set<(e: { taskId: string, state?: TaskState, progress?: number }) => void>()
365
+ const agentListeners = new Set<(e: { agentId: string, state: 'idle' | 'busy' | 'stopped' }) => void>()
366
+ const messageListeners = new Set<(message: A2AMessage) => void>()
367
+ const memoryListeners = new Set<(e: { agentId: string, scope: 'private' | 'shared', title: string, dedupKey: string }) => void>()
368
+ const memberListeners = new Set<(e: MemberChangeEvent) => void>()
369
+ return {
370
+ emit: (event, source) => {
371
+ for (const fn of eventListeners) {
372
+ try {
373
+ fn(event, source)
374
+ }
375
+ catch (err) {
376
+ log.error('[ChannelBus] event listener error:', err)
377
+ }
378
+ }
379
+ cr.wakeScheduler()
380
+ },
381
+ onEvent: (fn) => {
382
+ eventListeners.add(fn)
383
+ return () => eventListeners.delete(fn)
384
+ },
385
+ notifyTask: (e: { taskId: string, state?: TaskState, progress?: number, agentId?: string, task?: TaskEventTask }) => {
386
+ for (const fn of taskListeners) {
387
+ try {
388
+ fn(e)
389
+ }
390
+ catch (err) {
391
+ log.error('[ChannelBus] task listener error:', err)
392
+ }
393
+ }
394
+ },
395
+ onTaskEvent: (fn) => {
396
+ taskListeners.add(fn)
397
+ return () => taskListeners.delete(fn)
398
+ },
399
+ notifyAgent: (e) => {
400
+ for (const fn of agentListeners) {
401
+ try {
402
+ fn(e)
403
+ }
404
+ catch (err) {
405
+ log.error('[ChannelBus] agent listener error:', err)
406
+ }
407
+ }
408
+ },
409
+ onAgentStatus: (fn) => {
410
+ agentListeners.add(fn)
411
+ return () => agentListeners.delete(fn)
412
+ },
413
+ notifyMessage: (message) => {
414
+ for (const fn of messageListeners) {
415
+ try {
416
+ fn(message)
417
+ }
418
+ catch (err) {
419
+ log.error('[ChannelBus] message listener error:', err)
420
+ }
421
+ }
422
+ },
423
+ onMessage: (fn) => {
424
+ messageListeners.add(fn)
425
+ return () => messageListeners.delete(fn)
426
+ },
427
+ notifyMemory: (e) => {
428
+ for (const fn of memoryListeners) {
429
+ try {
430
+ fn(e)
431
+ }
432
+ catch (err) {
433
+ log.error('[ChannelBus] memory listener error:', err)
434
+ }
435
+ }
436
+ },
437
+ onMemoryEvent: (fn) => {
438
+ memoryListeners.add(fn)
439
+ return () => memoryListeners.delete(fn)
440
+ },
441
+ notifyMember: (e) => {
442
+ for (const fn of memberListeners) {
443
+ try {
444
+ fn(e)
445
+ }
446
+ catch (err) {
447
+ log.error('[ChannelBus] member listener error:', err)
448
+ }
449
+ }
450
+ },
451
+ onMemberEvent: (fn) => {
452
+ memberListeners.add(fn)
453
+ return () => memberListeners.delete(fn)
454
+ },
455
+ wakeScheduler: () => {
456
+ cr.wakeScheduler()
457
+ },
458
+ }
459
+ }
460
+
461
+ subscribeAgentStatus(channelId: string, fn: (e: Parameters<ChannelBus['notifyAgent']>[0]) => void): () => void {
462
+ // 先确保 bus 存在:channel 尚未激活时订阅会被静默丢弃(monitor 先订阅后提交任务的场景)
463
+ // 返回 bus 的真实退订函数:stream 重绑时必须可退订,否则同一 bus 上订阅两份 → 事件双发
464
+ this.ensureChannelRuntime(channelId)
465
+ return this.buses.get(channelId)?.onAgentStatus(fn) ?? (() => {})
466
+ }
467
+
468
+ subscribeChannelEvents(channelId: string, fn: (event: AgentEvent, source: A2AMessage) => void): () => void {
469
+ this.ensureChannelRuntime(channelId)
470
+ return this.buses.get(channelId)?.onEvent(fn) ?? (() => {})
471
+ }
472
+
473
+ subscribeTaskEvents(channelId: string, fn: (e: { taskId: string, state?: TaskState, progress?: number, agentId?: string, task?: TaskEventTask }) => void): () => void {
474
+ // 真实退订(同上:防 stream 重绑泄漏导致 task.status 双发落库)
475
+ this.ensureChannelRuntime(channelId)
476
+ return this.buses.get(channelId)?.onTaskEvent(fn) ?? (() => {})
477
+ }
478
+
479
+ /** 订阅 channel 内消息投递(AEP a2a.message 事件源;route 汇流点触发) */
480
+ subscribeChannelMessages(channelId: string, fn: (message: A2AMessage) => void): () => void {
481
+ this.ensureChannelRuntime(channelId)
482
+ return this.buses.get(channelId)?.onMessage(fn) ?? (() => {})
483
+ }
484
+
485
+ /** 订阅 channel 内记忆写入(AEP memory.saved 事件源) */
486
+ subscribeMemoryEvents(channelId: string, fn: (e: { agentId: string, scope: 'private' | 'shared', title: string, dedupKey: string }) => void): () => void {
487
+ this.ensureChannelRuntime(channelId)
488
+ return this.buses.get(channelId)?.onMemoryEvent(fn) ?? (() => {})
489
+ }
490
+
491
+ /** 订阅 channel 内团队成员增/改/删(AEP agent.member 事件源;lead 工具桥与 REST 入口共用汇流点) */
492
+ subscribeMemberEvents(channelId: string, fn: (e: MemberChangeEvent) => void): () => void {
493
+ this.ensureChannelRuntime(channelId)
494
+ return this.buses.get(channelId)?.onMemberEvent(fn) ?? (() => {})
495
+ }
496
+
497
+ private notifyTask(
498
+ channelId: string,
499
+ e: { taskId: string, state?: TaskState, progress?: number, agentId?: string, task?: TaskEventTask },
500
+ ): void {
501
+ this.buses.get(channelId)?.notifyTask(e)
502
+ }
503
+
504
+ /** 团队成员变更广播(AEP agent.member 事件源;lead 工具桥与 REST 入口共用) */
505
+ private notifyMember(channelId: string, e: MemberChangeEvent): void {
506
+ this.buses.get(channelId)?.notifyMember(e)
507
+ }
508
+
509
+ /** 按实例装配 AgentRuntime(每个实例一个独立运行时) */
510
+ private wireMember(m: ChannelAgentRow): AgentRuntime {
511
+ const agent = instanceToAgentInfo(m)
512
+ const cr = this.ensureChannelRuntime(m.channelId)
513
+ const bus = this.buses.get(m.channelId)!
514
+ const mailbox = new Mailbox(this.deps.repos.messages, m.channelId, agent.id, () => cr.wakeScheduler())
515
+ const memory = new AgentMemory(this.deps.repos.memories, { channelId: m.channelId, agentId: agent.id, embedder: this.memoryEmbedder ?? undefined })
516
+ const workspace = this.buildWorkspace(agent, memory)
517
+ const chWorkspace = this.channelWorkspace(m.channelId)
518
+ // channel 级作业场景 prompt 注入 harness config(用户场景 × 系统设计组合的入口;
519
+ // 变更经 updateChannel 回收成员运行时,下次装配拿到新场景)
520
+ const scenarioPrompt = this.deps.repos.channels.findById(m.channelId)?.scenarioPrompt ?? ''
521
+ const configWithCtx: Record<string, unknown> = { ...agent.config }
522
+ if (scenarioPrompt) configWithCtx.scenarioPrompt = scenarioPrompt
523
+ if (chWorkspace.length > 0) configWithCtx.cwd = chWorkspace
524
+ const agentWithCtx: AgentInfo = { ...agent, config: configWithCtx }
525
+ const runtime = new AgentRuntime(agent, this.deps.implFactory(agentWithCtx), {
526
+ mailbox,
527
+ taskEngine: this.getTaskEngine(),
528
+ bus,
529
+ workspace,
530
+ memory,
531
+ })
532
+ cr.addAgent(runtime)
533
+ this.agentIndex.set(runtimeKey(m.channelId, agent.id), runtime)
534
+ runtime.start()
535
+ return runtime
536
+ }
537
+
538
+ /** 按需装配实例运行时(幂等):已装配返回缓存,否则从实例行 wire;禁用/不存在返回 undefined */
539
+ private ensureAgentRuntime(channelId: string, agentId: string): AgentRuntime | undefined {
540
+ const key = runtimeKey(channelId, agentId)
541
+ const existing = this.agentIndex.get(key)
542
+ if (existing) return existing
543
+ const m = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
544
+ if (!m || m.enabled !== 1) return undefined
545
+ return this.wireMember(m)
546
+ }
547
+
548
+ private runtimeOf(channelId: string, agentId: string): AgentRuntime | undefined {
549
+ return this.agentIndex.get(runtimeKey(channelId, agentId))
550
+ }
551
+
552
+ /** 激活 channel:装配 lead 运行时 + 装配并启动 SchedulerLoop(幂等) */
553
+ ensureChannelActive(channelId: string, options?: SchedulerLoopOptions): void {
554
+ const channel = this.deps.repos.channels.findById(channelId)
555
+ if (!channel || channel.enabled !== 1 || !channel.leadAgentId) return
556
+ const cr = this.ensureChannelRuntime(channelId)
557
+ if (cr.scheduler) return
558
+ const lead = this.ensureAgentRuntime(channelId, channel.leadAgentId)
559
+ if (!lead) return
560
+ const loop = new SchedulerLoop(cr, lead, {
561
+ ...options,
562
+ // 调度快照的邮件上下文(lead 观察 worker 间通信的唯一来源;DB 为事实源)
563
+ supervisionMail: limit => this.deps.repos.messages
564
+ .listRecentByChannel(channelId, limit)
565
+ .map(rowToChannelMail),
566
+ })
567
+ loop.setLoopResubmitCallback((title, description) => {
568
+ this.submitChannelTask({ channelId, title, description }).catch((err) => {
569
+ // 清理竞态:channel 已删除/lead 已卸载时的到期重放 → 静默(NOT_FOUND 为预期)
570
+ const code = (err as { code?: string }).code
571
+ if (code === 'NOT_FOUND' || code === 'NO_LEAD_AGENT') return
572
+ log.error(`[AgentChannelManager:${channelId}] loop 重新提交失败:`, err)
573
+ })
574
+ })
575
+ cr.scheduler = loop
576
+ loop.start()
577
+ }
578
+
579
+ /** 停止并卸载某实例运行时(强制;删除实例/channel 时用) */
580
+ private async stopAndDetach(channelId: string, agentId: string): Promise<void> {
581
+ const key = runtimeKey(channelId, agentId)
582
+ const runtime = this.agentIndex.get(key)
583
+ if (!runtime) return
584
+ const cr = this.channels.get(channelId)
585
+ if (runtime.role === 'lead' && cr) {
586
+ cr.scheduler?.stop()
587
+ cr.scheduler = null
588
+ }
589
+ // 先摘除再停机:runtime.stop() 会关闭 mailbox,若成员仍在 route 视野内,
590
+ // 停机窗口内到达的消息会被 closed mailbox 静默吞掉但 delivered 照常上报
591
+ // (发送方收到成功假象);先 detach 让 route 立即按"成员不存在"如实失败
592
+ cr?.detachAgent(agentId)
593
+ await runtime.stop()
594
+ this.agentIndex.delete(key)
595
+ if (cr && cr.getAgents().length === 0 && !cr.scheduler) {
596
+ this.channels.delete(channelId)
597
+ this.buses.delete(channelId)
598
+ }
599
+ }
600
+
601
+ /** 卸载实例运行时(空闲后释放内存,杀 omp 子进程);busy/有 pending/lead 有活跃任务 → 跳过 */
602
+ async unloadAgent(channelId: string, agentId: string): Promise<void> {
603
+ const runtime = this.runtimeOf(channelId, agentId)
604
+ if (!runtime) return
605
+ if (runtime.getState() !== 'idle') return
606
+ if (this.deps.repos.messages.listPendingByChannelAgent(channelId, agentId).length > 0) return
607
+ if (runtime.role === 'lead') {
608
+ const tasks = this.getTaskEngine().list(runtime.channelId)
609
+ const hasActive = tasks.some(t => t.state !== 'COMPLETED' && t.state !== 'CANCELED' && t.state !== 'FAILED')
610
+ if (hasActive) return
611
+ }
612
+ await this.stopAndDetach(channelId, agentId)
613
+ }
614
+
615
+ async unloadIdleAgents(): Promise<void> {
616
+ for (const rt of [...this.agentIndex.values()]) {
617
+ await this.unloadAgent(rt.channelId, rt.agentId)
618
+ }
619
+ }
620
+
621
+ startIdleSweeper(options?: { intervalMs?: number, graceMs?: number }): () => void {
622
+ const intervalMs = options?.intervalMs ?? 60_000
623
+ const graceMs = options?.graceMs ?? 120_000
624
+ const idleSince = new Map<string, number>()
625
+ this.idleSweeperTimer = setInterval(() => {
626
+ const now = Date.now()
627
+ for (const rt of [...this.agentIndex.values()]) {
628
+ // 存活校准优先:休眠/强杀后子进程 exit 事件可能不达,周期探 OS 实际存在性,
629
+ // 死进程收敛为已退出(在途回合归位,下一回合自动重生),避免 stuck busy/死客户端空转
630
+ try {
631
+ rt.reconcileProcess()
632
+ }
633
+ catch (err) {
634
+ log.error(`[AgentChannelManager] 校准 ${rt.channelId}/${rt.agentId} 进程存活失败:`, err)
635
+ }
636
+ const key = runtimeKey(rt.channelId, rt.agentId)
637
+ if (rt.getState() === 'idle') {
638
+ const since = idleSince.get(key) ?? now
639
+ idleSince.set(key, since)
640
+ if (now - since >= graceMs) {
641
+ idleSince.delete(key)
642
+ this.unloadAgent(rt.channelId, rt.agentId).catch((err) => {
643
+ log.error(`[AgentChannelManager] 卸载 ${rt.channelId}/${rt.agentId} 失败:`, err)
644
+ })
645
+ }
646
+ }
647
+ else {
648
+ idleSince.delete(key)
649
+ }
650
+ }
651
+ }, intervalMs)
652
+ return () => {
653
+ if (this.idleSweeperTimer) {
654
+ clearInterval(this.idleSweeperTimer)
655
+ this.idleSweeperTimer = null
656
+ }
657
+ }
658
+ }
659
+
660
+ runtimeStatus(): { wiredAgents: string[], activeChannels: string[] } {
661
+ return {
662
+ wiredAgents: [...this.agentIndex.values()].map(rt => rt.agentId),
663
+ activeChannels: [...this.channels.keys()],
664
+ }
665
+ }
666
+
667
+ /**
668
+ * 运行时资源监控快照:已装配的 ChannelRuntime / AgentRuntime
669
+ * + 全部已启动的 harness 进程(注册表,含已脱离 runtimes 的孤儿进程)。
670
+ */
671
+ monitorRuntime(): RuntimeMonitorSnapshot {
672
+ sweepHarnessProcesses()
673
+ sweepTerminalSessions()
674
+ const channels: RuntimeChannelView[] = [...this.channels.values()].map((cr) => {
675
+ const channel = this.deps.repos.channels.findById(cr.channelId)
676
+ return {
677
+ channelId: cr.channelId,
678
+ wiredAgentCount: cr.getAgents().length,
679
+ memberCount: this.deps.repos.channelAgents.listByChannel(cr.channelId).length,
680
+ hasScheduler: cr.scheduler !== null,
681
+ leadAgentId: channel?.leadAgentId ?? null,
682
+ ownerUserId: channel?.ownerUserId ?? null,
683
+ }
684
+ })
685
+ const agents: RuntimeAgentView[] = [...this.agentIndex.values()].map((rt) => {
686
+ const status = rt.getStatus()
687
+ const row = this.deps.repos.channelAgents.findByChannelAgent(rt.channelId, rt.agentId)
688
+ return {
689
+ channelId: rt.channelId,
690
+ agentId: rt.agentId,
691
+ name: rt.name,
692
+ role: rt.role,
693
+ harness: row?.harness ?? '?',
694
+ state: status.state,
695
+ currentTaskId: status.currentTaskId,
696
+ queuedCount: status.queuedCount,
697
+ completedCount: status.completedCount,
698
+ process: rt.getProcessInfo(),
699
+ }
700
+ })
701
+ const channelOwner = new Map(channels.map(c => [c.channelId, c.ownerUserId ?? null]))
702
+ const boundPids = new Set(
703
+ agents.map(a => a.process?.pid).filter((p): p is number => typeof p === 'number'),
704
+ )
705
+ const processes: RuntimeProcessView[] = listHarnessProcesses().map(p => ({
706
+ pid: p.pid,
707
+ harness: p.harness,
708
+ command: p.command,
709
+ args: p.args,
710
+ agentId: p.agentId,
711
+ channelId: p.channelId,
712
+ name: p.name,
713
+ role: p.role,
714
+ startedAt: p.startedAt,
715
+ alive: p.alive,
716
+ exitCode: p.exitCode,
717
+ bound: boundPids.has(p.pid),
718
+ terminal: hasTerminalSession(p.pid),
719
+ }))
720
+ for (const a of agents) {
721
+ a.ownerUserId = channelOwner.get(a.channelId) ?? null
722
+ }
723
+ for (const p of processes) {
724
+ ;(p as RuntimeProcessView & { ownerUserId?: string | null }).ownerUserId = p.channelId
725
+ ? channelOwner.get(p.channelId) ?? null
726
+ : null
727
+ }
728
+ return {
729
+ generatedAt: new Date().toISOString(),
730
+ serverPid: process.pid,
731
+ uptimeMs: Math.round(process.uptime() * 1000),
732
+ channels,
733
+ agents,
734
+ processes,
735
+ counts: {
736
+ channels: channels.length,
737
+ agents: agents.length,
738
+ processes: processes.length,
739
+ aliveProcesses: processes.filter(p => p.alive).length,
740
+ orphanProcesses: processes.filter(p => p.alive && !p.bound).length,
741
+ },
742
+ }
743
+ }
744
+
745
+ /**
746
+ * 用户级监控快照(v10 用户隔离):
747
+ * - 普通用户:仅本人 channel 的 runtime/agent,以及绑定到本人 channel 的进程;
748
+ * 未绑定/孤儿进程与遗留无主 channel 一律不可见。
749
+ * - admin:全量视图,每个 channel/agent/process 附带 ownerUserId(ownerName 由路由层补注)。
750
+ */
751
+ monitorRuntimeForUser(user: ActingUser): RuntimeMonitorSnapshot & { scope: 'user' | 'admin', ownerNames?: Record<string, string> } {
752
+ const snap = this.monitorRuntime()
753
+ if (user.role === 'admin') {
754
+ const ownerIds = new Set<string>()
755
+ for (const c of snap.channels) {
756
+ if (c.ownerUserId) ownerIds.add(c.ownerUserId)
757
+ }
758
+ const ownerNames: Record<string, string> = {}
759
+ for (const id of ownerIds) {
760
+ const name = resolveOwnerName(id)
761
+ if (name) ownerNames[id] = name
762
+ }
763
+ for (const c of snap.channels) c.ownerName = c.ownerUserId ? ownerNames[c.ownerUserId] ?? null : null
764
+ for (const a of snap.agents) a.ownerName = a.ownerUserId ? ownerNames[a.ownerUserId] ?? null : null
765
+ return { ...snap, scope: 'admin', ownerNames }
766
+ }
767
+ const visibleChannels = new Set(snap.channels.filter(c => c.ownerUserId === user.id).map(c => c.channelId))
768
+ // 本人有主但当前未装配 runtime 的 channel 也计入 channels 视图(成员数来自 DB,装配为 0)
769
+ for (const row of this.deps.repos.channels.list()) {
770
+ if (row.ownerUserId === user.id) visibleChannels.add(row.id)
771
+ }
772
+ const channels = snap.channels.filter(c => visibleChannels.has(c.channelId))
773
+ for (const row of this.deps.repos.channels.list()) {
774
+ if (visibleChannels.has(row.id) && !channels.some(c => c.channelId === row.id)) {
775
+ channels.push({
776
+ channelId: row.id,
777
+ wiredAgentCount: 0,
778
+ memberCount: this.deps.repos.channelAgents.listByChannel(row.id).length,
779
+ hasScheduler: false,
780
+ leadAgentId: row.leadAgentId,
781
+ ownerUserId: row.ownerUserId,
782
+ })
783
+ }
784
+ }
785
+ const agents = snap.agents.filter(a => visibleChannels.has(a.channelId))
786
+ const processes = snap.processes.filter(p => p.channelId !== null && visibleChannels.has(p.channelId))
787
+ return {
788
+ generatedAt: snap.generatedAt,
789
+ serverPid: snap.serverPid,
790
+ uptimeMs: snap.uptimeMs,
791
+ channels,
792
+ agents,
793
+ processes,
794
+ counts: {
795
+ channels: channels.length,
796
+ agents: agents.length,
797
+ processes: processes.length,
798
+ aliveProcesses: processes.filter(p => p.alive).length,
799
+ orphanProcesses: processes.filter(p => p.alive && !p.bound).length,
800
+ },
801
+ scope: 'user',
802
+ }
803
+ }
804
+
805
+ /**
806
+ * 终止指定 runtime 的 harness 进程 → 对应 AgentRuntime 随之 stop 并卸载。
807
+ * 语义比 HITL stopAgentRuntime 更强:先强杀进程(进程树),再走 stopAndDetach
808
+ * (停 SchedulerLoop / 中断当前 run / dispose impl / 移出索引)。成员行保留,
809
+ * 后续任务投递按需重新装配。
810
+ * runtime 未装配(如已被空闲卸载)但进程残留 → 按 agentId 兜底强杀进程,防资源浪费。
811
+ */
812
+ async terminateRuntimeProcess(channelId: string, agentId: string): Promise<{ agentId: string, stopped: boolean }> {
813
+ const m = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
814
+ if (!m) throw new AppError(404, 'NOT_FOUND', `成员不存在: ${agentId}`)
815
+ const runtime = this.runtimeOf(channelId, agentId)
816
+ if (runtime) {
817
+ try {
818
+ runtime.killProcess()
819
+ }
820
+ catch (err) {
821
+ log.error(`[AgentChannelManager] 终止进程失败 ${channelId}/${agentId}:`, err)
822
+ }
823
+ await this.stopAndDetach(channelId, agentId)
824
+ return { agentId, stopped: true }
825
+ }
826
+ const leftover = listAliveHarnessProcessesByAgent(agentId)
827
+ for (const p of leftover) killHarnessProcess(p.pid)
828
+ return { agentId, stopped: leftover.length > 0 }
829
+ }
830
+
831
+ /** 按 PID 终止 harness 进程(孤儿进程专用;已绑定 runtime 的请走 terminateRuntimeProcess) */
832
+ killHarnessProcessByPid(pid: number): { pid: number, killed: boolean } {
833
+ return { pid, killed: killHarnessProcess(pid) }
834
+ }
835
+
836
+ async shutdown(): Promise<void> {
837
+ if (this.idleSweeperTimer) {
838
+ clearInterval(this.idleSweeperTimer)
839
+ this.idleSweeperTimer = null
840
+ }
841
+ if (this.memoryTimer) {
842
+ clearInterval(this.memoryTimer)
843
+ this.memoryTimer = null
844
+ }
845
+ for (const cr of this.channels.values()) {
846
+ cr.scheduler?.stop()
847
+ cr.scheduler = null
848
+ }
849
+ await Promise.all([...this.agentIndex.values()].map(a => a.stop()))
850
+ this.agentIndex.clear()
851
+ this.channels.clear()
852
+ this.buses.clear()
853
+ }
854
+
855
+ /** Agent 自主作业能力面(绑定本实例身份与 channel,委托 manager 作业方法) */
856
+ private buildWorkspace(agent: AgentInfo, memory: AgentMemory): AgentWorkspace {
857
+ const channelId = agent.channelId
858
+ return {
859
+ listAgents: () => this.listChannelAgents(channelId),
860
+ dispatchTask: input => this.dispatchTask(channelId, agent.id, input),
861
+ listTasks: () => this.listTasks(channelId, agent.id),
862
+ getTask: taskId => this.getTask(channelId, agent.id, taskId),
863
+ reportTask: input => this.reportTask(channelId, agent.id, input),
864
+ completeTask: (taskId, artifacts) => this.completeTask(channelId, agent.id, { taskId, artifacts }),
865
+ cancelTask: taskId => this.cancelTask(channelId, agent.id, { taskId }),
866
+ myQueue: () => this.myQueue(channelId, agent.id),
867
+ queueOverview: () => this.queueOverview(channelId, agent.id),
868
+ updateTask: (taskId, patch) => this.updateTask(channelId, agent.id, taskId, patch),
869
+ reassignTask: (taskId, toAgentId) => this.reassignTask(channelId, agent.id, taskId, toAgentId),
870
+ sendMessage: input => this.sendA2A(channelId, agent.id, input),
871
+ refuseTask: (taskId, reason) => this.refuseTask(channelId, agent.id, taskId, reason),
872
+ pollMailbox: limit => this.pollMailbox(channelId, agent.id, limit),
873
+ waitMailbox: (limit, waitMs) => this.waitMailbox(channelId, agent.id, limit ?? 10, waitMs ?? 0),
874
+ ackMailbox: ids => Promise.resolve(this.ackMailbox(channelId, agent.id, ids)),
875
+ listMail: opts => this.listChannelMail(channelId, agent.id, opts),
876
+ subscribe: input => this.subscribe(channelId, agent.id, input),
877
+ // 记忆按需抓取/主动沉淀(成员校验 + 委托本实例 AgentMemory;shared 写入即 Channel 公共域)
878
+ recallMemory: async (input) => {
879
+ this.requireMember(channelId, agent.id)
880
+ return memory.recallRows(input.query, { scope: input.scope, limit: input.limit })
881
+ },
882
+ saveMemory: async (input) => {
883
+ this.requireMember(channelId, agent.id)
884
+ const saved = await memory.save(input)
885
+ this.buses.get(channelId)?.notifyMemory({ agentId: agent.id, scope: input.scope, title: input.title, dedupKey: saved.dedupKey })
886
+ return saved
887
+ },
888
+ // 团队成员管理(仅 lead;manager 内二次校验角色,工具面与决策面共用)
889
+ createTeamMember: input => this.createTeamMember(channelId, agent.id, input),
890
+ updateTeamMember: (agentId, patch) => this.updateTeamMember(channelId, agent.id, agentId, patch),
891
+ removeTeamMember: (agentId, reason) => this.removeTeamMember(channelId, agent.id, agentId, reason),
892
+ }
893
+ }
894
+
895
+ // ===== 用户面(用户级隔离;管理 API 凭证 = 用户 token)=====
896
+
897
+ /** 注册用户(name 唯一 → 409;token 仅此一次返回) */
898
+ registerUser(name: string): UserRow {
899
+ const trimmed = name.trim()
900
+ if (!trimmed) throw new AppError(400, 'BAD_REQUEST', '用户名不能为空')
901
+ if (this.deps.repos.users.getByName(trimmed)) {
902
+ throw new AppError(409, 'USER_EXISTS', `用户名已存在: ${trimmed}`)
903
+ }
904
+ return this.deps.repos.users.create(trimmed)
905
+ }
906
+
907
+ /** 用户 token → 用户(无效 → 401;REST resolveUser / WS sub 共用) */
908
+ getUserByToken(token: string): UserRow | null {
909
+ return this.deps.repos.users.getByToken(token)
910
+ }
911
+
912
+ /**
913
+ * 资源 owner 守卫:owner 匹配放行;NULL owner(遗留公共数据)只读——
914
+ * 写操作一律拒绝(FORBIDDEN_LEGACY,提示归属缺失);他人资源 → 403。
915
+ */
916
+ requireOwned(ownerUserId: string | null | undefined, userId: string, what: string): void {
917
+ if (ownerUserId === null || ownerUserId === undefined) {
918
+ throw new AppError(403, 'FORBIDDEN_LEGACY', `${what} 为遗留公共数据(无归属),禁止变更`)
919
+ }
920
+ if (ownerUserId !== userId) {
921
+ throw new AppError(403, 'SCOPE_VIOLATION', `${what} 不属于当前用户`)
922
+ }
923
+ }
924
+
925
+ /**
926
+ * 模板/资源可写守卫(v10 权限系统):
927
+ * - 内置模板(owner NULL)→ 任何人(含 admin)不可修改删除(TEMPLATE_BUILTIN);
928
+ * - 属主 → 放行;
929
+ * - admin → 越权放行(最高管理权限);
930
+ * - 其余 → 403 SCOPE_VIOLATION。
931
+ */
932
+ requireWritable(ownerUserId: string | null | undefined, user: ActingUser, what: string): void {
933
+ if (ownerUserId === null || ownerUserId === undefined) {
934
+ throw new AppError(403, 'TEMPLATE_BUILTIN', `${what} 为内置公共模板,不可修改或删除`)
935
+ }
936
+ if (ownerUserId === user.id) return
937
+ if (user.role === 'admin') return
938
+ throw new AppError(403, 'SCOPE_VIOLATION', `${what} 不属于当前用户`)
939
+ }
940
+
941
+ /**
942
+ * 模板可读守卫:属主 / public / admin 放行;他人 private → 403(不存在语义由调用方先判)。
943
+ */
944
+ requireTemplateReadable(tpl: { ownerUserId: string | null, visibility: string }, user: ActingUser, what: string): void {
945
+ if (tpl.ownerUserId === null || tpl.ownerUserId === user.id) return
946
+ if (tpl.visibility === 'public') return
947
+ if (user.role === 'admin') return
948
+ throw new AppError(403, 'SCOPE_VIOLATION', `${what} 不属于当前用户且未公开`)
949
+ }
950
+
951
+ /** channel 读取(已认证用户可见本人 + 遗留公共;不存在 → 404) */
952
+ getChannelForUser(channelId: string, userId: string): ChannelRow {
953
+ const channel = this.deps.repos.channels.findById(channelId)
954
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
955
+ if (channel.ownerUserId !== null && channel.ownerUserId !== userId) {
956
+ throw new AppError(403, 'SCOPE_VIOLATION', 'channel 不属于当前用户')
957
+ }
958
+ return channel
959
+ }
960
+
961
+ /** 用户视角 channel 列表(本人 + 遗留公共) */
962
+ listChannelsForUser(userId: string): ChannelRow[] {
963
+ return this.deps.repos.channels.listForOwner(userId)
964
+ }
965
+
966
+ // ===== Workspace(服务端持久化;按 owner 隔离)=====
967
+
968
+ listWorkspaces(userId: string): Array<WorkspaceRow & { channelIds: string[] }> {
969
+ return this.deps.repos.users.listWorkspaces(userId).map(ws => ({
970
+ ...ws,
971
+ // 挂载引用自愈:channel 删除(removeChannel)不级联清理 workspace_channels,
972
+ // 死引用会让前端订阅永不到达(左栏"幽灵频道"+ 右栏空数据假象)——
973
+ // 读取时过滤并顺手删除挂载行,历史脏数据随读取收敛
974
+ channelIds: this.deps.repos.users.listMountedChannels(ws.id).filter((id) => {
975
+ if (this.deps.repos.channels.findById(id)) return true
976
+ this.deps.repos.users.unmountChannel(ws.id, id)
977
+ return false
978
+ }),
979
+ }))
980
+ }
981
+
982
+ createWorkspace(userId: string, name: string): WorkspaceRow & { channelIds: string[] } {
983
+ const trimmed = name.trim()
984
+ if (!trimmed) throw new AppError(400, 'BAD_REQUEST', 'Workspace 名称不能为空')
985
+ const ws = this.deps.repos.users.createWorkspace(userId, trimmed)
986
+ return { ...ws, channelIds: [] }
987
+ }
988
+
989
+ deleteWorkspace(userId: string, workspaceId: string): void {
990
+ const ws = this.deps.repos.users.getWorkspace(workspaceId)
991
+ if (!ws) throw new AppError(404, 'NOT_FOUND', `workspace 不存在: ${workspaceId}`)
992
+ this.requireOwned(ws.ownerUserId, userId, 'workspace')
993
+ this.deps.repos.users.deleteWorkspace(workspaceId)
994
+ }
995
+
996
+ /** 挂载 channel(须为本人的 channel;遗留公共不可挂载) */
997
+ mountChannelToWorkspace(userId: string, workspaceId: string, channelId: string): void {
998
+ const ws = this.deps.repos.users.getWorkspace(workspaceId)
999
+ if (!ws) throw new AppError(404, 'NOT_FOUND', `workspace 不存在: ${workspaceId}`)
1000
+ this.requireOwned(ws.ownerUserId, userId, 'workspace')
1001
+ const channel = this.deps.repos.channels.findById(channelId)
1002
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
1003
+ this.requireOwned(channel.ownerUserId, userId, 'channel')
1004
+ this.deps.repos.users.mountChannel(workspaceId, channelId)
1005
+ }
1006
+
1007
+ unmountChannelFromWorkspace(userId: string, workspaceId: string, channelId: string): void {
1008
+ const ws = this.deps.repos.users.getWorkspace(workspaceId)
1009
+ if (!ws) throw new AppError(404, 'NOT_FOUND', `workspace 不存在: ${workspaceId}`)
1010
+ this.requireOwned(ws.ownerUserId, userId, 'workspace')
1011
+ this.deps.repos.users.unmountChannel(workspaceId, channelId)
1012
+ }
1013
+
1014
+ // ===== Channel 管理面 =====
1015
+
1016
+ async createChannel(input: {
1017
+ name: string
1018
+ description?: string
1019
+ scenarioPrompt?: string
1020
+ workspace?: string
1021
+ leadAgent?: { name: string, harness: string, config?: Record<string, unknown> }
1022
+ ownerUserId?: string | null
1023
+ }): Promise<{ channelId: string, leadAgentId?: string, workspace: string }> {
1024
+ const channel = this.deps.repos.channels.create({ name: input.name, description: input.description, scenarioPrompt: input.scenarioPrompt, ownerUserId: input.ownerUserId ?? null })
1025
+ const workspace = input.workspace && input.workspace.length > 0
1026
+ ? input.workspace
1027
+ : resolve(process.cwd(), 'data', 'workspaces', channel.id)
1028
+ this.deps.repos.channels.update(channel.id, { workspace })
1029
+ this.ensureWorkspaceDir(workspace)
1030
+
1031
+ let leadAgentId: string | undefined
1032
+ if (input.leadAgent) {
1033
+ // lead 内联模板归属 channel 属主(private;避免落成无主内置模板)
1034
+ const tpl = this.deps.repos.agents.create({
1035
+ name: input.leadAgent.name,
1036
+ harness: input.leadAgent.harness,
1037
+ config: input.leadAgent.config,
1038
+ ownerUserId: input.ownerUserId ?? null,
1039
+ })
1040
+ const inst = this.deps.repos.channelAgents.create({
1041
+ channelId: channel.id,
1042
+ templateId: tpl.id,
1043
+ name: tpl.name,
1044
+ harness: tpl.harness,
1045
+ config: parseJson<Record<string, unknown>>(tpl.configJson, {}),
1046
+ role: 'lead',
1047
+ })
1048
+ this.deps.repos.channels.update(channel.id, { leadAgentId: inst.id })
1049
+ leadAgentId = inst.id
1050
+ // 懒加载:仅持久化,不装配运行时;首次任务提交时 ensureChannelActive 触发装配
1051
+ }
1052
+ return { channelId: channel.id, leadAgentId, workspace }
1053
+ }
1054
+
1055
+ async getChannel(channelId: string): Promise<ChannelRow & { agents: AgentInfo[] }> {
1056
+ const channel = this.deps.repos.channels.findById(channelId)
1057
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
1058
+ const agents = await this.listChannelAgents(channelId)
1059
+ return { ...channel, agents }
1060
+ }
1061
+
1062
+ async updateChannel(channelId: string, patch: { name?: string, description?: string, scenarioPrompt?: string, workspace?: string, enabled?: number }): Promise<ChannelRow> {
1063
+ const channel = this.deps.repos.channels.findById(channelId)
1064
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
1065
+ if (patch.workspace !== undefined && patch.workspace !== channel.workspace) {
1066
+ this.ensureWorkspaceDir(patch.workspace)
1067
+ await this.unloadChannelAgents(channelId)
1068
+ }
1069
+ if (patch.enabled === 0 && channel.enabled !== 0) {
1070
+ await this.unloadChannelAgents(channelId)
1071
+ }
1072
+ // 场景 prompt 变更 → 回收成员运行时(下次装配注入新场景;进度任务不受影响,
1073
+ // 排队任务在成员重新装配后按需恢复)
1074
+ if (patch.scenarioPrompt !== undefined && patch.scenarioPrompt !== channel.scenarioPrompt) {
1075
+ await this.unloadChannelAgents(channelId)
1076
+ }
1077
+ const updated = this.deps.repos.channels.update(channelId, patch)
1078
+ // 卸载后 channel 需重新激活(懒装配 lead + 恢复调度驱动)
1079
+ if (updated && updated.leadAgentId && (patch.scenarioPrompt !== undefined || patch.workspace !== undefined)) {
1080
+ this.ensureChannelActive(channelId)
1081
+ }
1082
+ return updated!
1083
+ }
1084
+
1085
+ private async unloadChannelAgents(channelId: string): Promise<void> {
1086
+ const cr = this.channels.get(channelId)
1087
+ if (cr) {
1088
+ cr.scheduler?.stop()
1089
+ cr.scheduler = null
1090
+ for (const agent of [...cr.getAgents()]) {
1091
+ await agent.stop()
1092
+ cr.detachAgent(agent.agentId)
1093
+ this.agentIndex.delete(runtimeKey(channelId, agent.agentId))
1094
+ }
1095
+ if (cr.getAgents().length === 0) {
1096
+ this.channels.delete(channelId)
1097
+ this.buses.delete(channelId)
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ async updateChannelWorkspace(channelId: string, workspace: string): Promise<ChannelRow> {
1103
+ return this.updateChannel(channelId, { workspace })
1104
+ }
1105
+
1106
+ private ensureWorkspaceDir(workspace: string): void {
1107
+ mkdirSync(workspace, { recursive: true })
1108
+ }
1109
+
1110
+ private channelWorkspace(channelId: string): string {
1111
+ return this.deps.repos.channels.findById(channelId)?.workspace ?? ''
1112
+ }
1113
+
1114
+ async listChannels(): Promise<ChannelRow[]> {
1115
+ return this.deps.repos.channels.list()
1116
+ }
1117
+
1118
+ async removeChannel(channelId: string): Promise<void> {
1119
+ const cr = this.channels.get(channelId)
1120
+ if (cr) {
1121
+ cr.scheduler?.stop()
1122
+ cr.scheduler = null
1123
+ for (const agent of [...cr.getAgents()]) {
1124
+ await agent.stop()
1125
+ this.agentIndex.delete(runtimeKey(channelId, agent.agentId))
1126
+ }
1127
+ this.channels.delete(channelId)
1128
+ this.buses.delete(channelId)
1129
+ }
1130
+ // 记忆级联清理:成员私有行 + team 公共行(防残留行污染他 channel 的 FTS/team 检索域)
1131
+ this.deps.repos.memories.deleteByChannel(channelId)
1132
+ this.deps.repos.channels.remove(channelId)
1133
+ }
1134
+
1135
+ // ===== Agent 模板管理面(全局,可复用;v10 可见性隔离) =====
1136
+
1137
+ /** 创建 Agent 模板(可复用数据结构;visibility 缺省 private) */
1138
+ async createAgent(input: {
1139
+ name: string
1140
+ harness: string
1141
+ config?: Record<string, unknown>
1142
+ visibility?: string
1143
+ ownerUserId?: string | null
1144
+ }): Promise<AgentTemplateDetail> {
1145
+ const row = this.deps.repos.agents.create({ name: input.name, harness: input.harness, config: input.config, visibility: input.visibility, ownerUserId: input.ownerUserId ?? null })
1146
+ return this.templateDetailOf(row)
1147
+ }
1148
+
1149
+ /** 列出全部 Agent 模板(全局) */
1150
+ async listAgents(): Promise<AgentTemplateDetail[]> {
1151
+ return this.deps.repos.agents.list().map(row => this.templateDetailOf(row))
1152
+ }
1153
+
1154
+ /** 用户视角模板列表(本人 + 遗留公共) */
1155
+ async listAgentsForUser(userId: string): Promise<AgentTemplateDetail[]> {
1156
+ return this.deps.repos.agents.listForOwner(userId).map(row => this.templateDetailOf(row))
1157
+ }
1158
+
1159
+ /** 可见性感知列表:普通用户 = 本人(任意可见性)+ 全部 public(含内置);admin = 全量 */
1160
+ async listAgentsVisibleTo(user: ActingUser): Promise<AgentTemplateDetail[]> {
1161
+ const rows = user.role === 'admin'
1162
+ ? this.deps.repos.agents.list()
1163
+ : this.deps.repos.agents.listVisible(user.id)
1164
+ return rows.map(row => this.templateDetailOf(row))
1165
+ }
1166
+
1167
+ /** Agent 模板详情(含其克隆出的全部实例) */
1168
+ getAgent(agentId: string): AgentTemplateDetail | undefined {
1169
+ const row = this.deps.repos.agents.findById(agentId)
1170
+ if (!row) return undefined
1171
+ return this.templateDetailOf(row)
1172
+ }
1173
+
1174
+ /** 更新 Agent 模板(name/harness/config/enabled/visibility);不影响已克隆实例(复制语义) */
1175
+ async updateAgent(agentId: string, patch: { name?: string, harness?: string, config?: Record<string, unknown>, enabled?: number, visibility?: string }): Promise<AgentTemplateDetail> {
1176
+ const row = this.deps.repos.agents.findById(agentId)
1177
+ if (!row) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${agentId}`)
1178
+ const updated = this.deps.repos.agents.update(agentId, patch)
1179
+ if (!updated) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${agentId}`)
1180
+ return this.templateDetailOf(updated)
1181
+ }
1182
+
1183
+ /** 删除 Agent 模板(实例保留,template_id 置空) */
1184
+ async removeAgent(agentId: string): Promise<void> {
1185
+ if (!this.deps.repos.agents.findById(agentId)) return
1186
+ this.deps.repos.agents.remove(agentId)
1187
+ }
1188
+
1189
+ private templateDetailOf(row: AgentRow): AgentTemplateDetail {
1190
+ const instances = this.deps.repos.channelAgents.listByTemplate(row.id)
1191
+ return {
1192
+ id: row.id,
1193
+ name: row.name,
1194
+ harness: row.harness,
1195
+ config: parseJson<Record<string, unknown>>(row.configJson, {}),
1196
+ enabled: row.enabled,
1197
+ visibility: row.ownerUserId === null ? 'public' : row.visibility,
1198
+ isBuiltin: row.ownerUserId === null,
1199
+ ownerUserId: row.ownerUserId ?? null,
1200
+ instances: instances.map(i => ({ id: i.id, channelId: i.channelId, role: i.role as 'lead' | 'worker', token: i.token })),
1201
+ createdAt: row.createdAt,
1202
+ updatedAt: row.updatedAt,
1203
+ }
1204
+ }
1205
+
1206
+ // ===== Channel 实例管理面 =====
1207
+
1208
+ /** 把 Agent 模板放入 channel → 克隆出独立身份 id 的新实例(复制 name/harness/config) */
1209
+ async addAgentToChannel(input: {
1210
+ channelId: string
1211
+ agentId: string
1212
+ role: 'lead' | 'worker'
1213
+ /** 模板 config 覆盖项(浅合并;用于注入/覆盖 systemPromptPrefix 等场景配置) */
1214
+ configOverride?: Record<string, unknown>
1215
+ /** 操作发起方(AEP agent.member 的 by;缺省 'user') */
1216
+ by?: string
1217
+ reason?: string
1218
+ }): Promise<AgentInfo> {
1219
+ const tpl = this.deps.repos.agents.findById(input.agentId)
1220
+ if (!tpl) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${input.agentId}`)
1221
+ if (input.role === 'lead') {
1222
+ const channel = this.deps.repos.channels.findById(input.channelId)
1223
+ if (channel && channel.leadAgentId) {
1224
+ throw new AppError(409, 'LEAD_EXISTS', `channel ${input.channelId} 已存在 lead`)
1225
+ }
1226
+ }
1227
+ const inst = this.deps.repos.channelAgents.create({
1228
+ channelId: input.channelId,
1229
+ templateId: tpl.id,
1230
+ name: tpl.name,
1231
+ harness: tpl.harness,
1232
+ config: { ...parseJson<Record<string, unknown>>(tpl.configJson, {}), ...input.configOverride },
1233
+ role: input.role,
1234
+ })
1235
+ if (input.role === 'lead') {
1236
+ this.deps.repos.channels.update(input.channelId, { leadAgentId: inst.id })
1237
+ }
1238
+ this.notifyMember(input.channelId, {
1239
+ op: 'added',
1240
+ agentId: inst.id,
1241
+ name: inst.name,
1242
+ role: inst.role as 'lead' | 'worker',
1243
+ harness: inst.harness,
1244
+ enabled: inst.enabled,
1245
+ config: parseJson<Record<string, unknown>>(inst.configJson, {}),
1246
+ by: input.by ?? 'user',
1247
+ reason: input.reason,
1248
+ })
1249
+ return instanceToAgentInfo(inst)
1250
+ }
1251
+
1252
+ async listChannelAgents(channelId: string): Promise<AgentInfo[]> {
1253
+ return this.deps.repos.channelAgents.listByChannel(channelId).map(instanceToAgentInfo)
1254
+ }
1255
+
1256
+ /** Agent 记忆列表(私有观察面;content 还原为未切分原文,客户端可读):实例须存在于本 channel */
1257
+ listMemories(channelId: string, agentId: string, limit = 50): MemoryRow[] {
1258
+ const m = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
1259
+ if (!m) throw new AppError(404, 'NOT_FOUND', `实例不存在: ${agentId}`)
1260
+ return this.deps.repos.memories.listByAgent(agentId, limit)
1261
+ .map(r => ({ ...r, content: unsegmentCJK(r.content) }))
1262
+ }
1263
+
1264
+ /**
1265
+ * 记忆混合检索(REST/客户端观察面;与 agent 运行时 search_memory 工具同源算法):
1266
+ * 以 targetAgent 视角召回(私有域 + 本 channel 公共域),scope 过滤同 recallRows。
1267
+ * caller 须为本 channel 成员;返回结构化片段(综合分排序,content 未切分原文)。
1268
+ */
1269
+ async searchAgentMemories(
1270
+ channelId: string,
1271
+ callerAgentId: string,
1272
+ targetAgentId: string,
1273
+ input: { query: string, scope?: 'auto' | 'private' | 'shared', limit?: number },
1274
+ opts: { byOwner?: boolean } = {},
1275
+ ): Promise<MemorySnippet[]> {
1276
+ // byOwner:人类 owner(channel 属主)经控制台检索,跳过 agent 成员校验
1277
+ if (!opts.byOwner) this.requireMember(channelId, callerAgentId)
1278
+ if (!this.deps.repos.channelAgents.findByChannelAgent(channelId, targetAgentId)) {
1279
+ throw new AppError(404, 'NOT_FOUND', `Agent 实例不存在: ${targetAgentId}`)
1280
+ }
1281
+ const mem = new AgentMemory(this.deps.repos.memories, { channelId, agentId: targetAgentId, embedder: this.memoryEmbedder ?? undefined })
1282
+ return mem.recallRows(input.query, { scope: input.scope, limit: input.limit })
1283
+ }
1284
+
1285
+ // ===== 团队共享记忆域(agent_id='__team__' 哨兵;lead 策展,channel 内全员 recall 可见)=====
1286
+
1287
+ /** 团队共享记忆列表(channel 级;任意本 channel 成员可读;content 还原为未切分原文) */
1288
+ listTeamMemories(channelId: string, limit = 50): MemoryRow[] {
1289
+ if (!this.deps.repos.channels.findById(channelId)) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
1290
+ return this.deps.repos.memories.listByAgentChannel(channelId, TEAM_AGENT_ID, limit)
1291
+ .map(r => ({ ...r, content: unsegmentCJK(r.content) }))
1292
+ }
1293
+
1294
+ /** 写/更新团队记忆(仅 lead;稳定 dedupKey 幂等刷新;成功后 fire-and-forget 向量化) */
1295
+ addTeamMemory(channelId: string, callerAgentId: string, input: { title: string, content: string, importance?: number, dedupKey?: string }, opts: { byOwner?: boolean } = {}): MemoryRow[] {
1296
+ // byOwner:人类 owner 策展(与 lead 同权;curator 身份以 __team__ 哨兵入事件)
1297
+ if (!opts.byOwner) {
1298
+ const caller = this.deps.repos.channelAgents.findByChannelAgent(channelId, callerAgentId)
1299
+ if (!caller || caller.role !== 'lead') throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可写团队记忆')
1300
+ }
1301
+ const dedupKey = input.dedupKey ?? `manual:${randomUUID()}`
1302
+ this.deps.repos.memories.upsert({
1303
+ channelId,
1304
+ agentId: TEAM_AGENT_ID,
1305
+ kind: 'semantic',
1306
+ title: input.title,
1307
+ titleFts: segmentCJK(input.title),
1308
+ content: segmentCJK(input.content).slice(0, 800),
1309
+ importance: input.importance ?? 0.9,
1310
+ taskId: null,
1311
+ dedupKey,
1312
+ })
1313
+ // 策展行同样入向量域(未切分原文;provider 未配置/失败 → 静默留 FTS)
1314
+ void vectorizeMemory(this.deps.repos.memories, this.memoryEmbedder, channelId, TEAM_AGENT_ID, dedupKey, input.content).catch(() => {})
1315
+ this.buses.get(channelId)?.notifyMemory({ agentId: callerAgentId, scope: 'shared', title: input.title, dedupKey })
1316
+ return this.listTeamMemories(channelId)
1317
+ }
1318
+
1319
+ /** 删团队记忆(仅 lead;vec 行残留由维护任务统一清理) */
1320
+ deleteTeamMemory(channelId: string, callerAgentId: string, memoryId: string, opts: { byOwner?: boolean } = {}): void {
1321
+ if (!opts.byOwner) {
1322
+ const caller = this.deps.repos.channelAgents.findByChannelAgent(channelId, callerAgentId)
1323
+ if (!caller || caller.role !== 'lead') throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可删团队记忆')
1324
+ }
1325
+ const row = this.deps.repos.memories.listByAgentChannel(channelId, TEAM_AGENT_ID, 1_000_000).find(r => r.id === memoryId)
1326
+ if (!row) throw new AppError(404, 'NOT_FOUND', `团队记忆不存在: ${memoryId}`)
1327
+ this.deps.repos.memories.delete(memoryId)
1328
+ }
1329
+
1330
+ /** 手动触发记忆衰减清理(REST 透传;策略与定时器同一函数) */
1331
+ runMemoryMaintenanceNow(): MaintenanceResult {
1332
+ return runMemoryMaintenance(this.deps.repos.memories)
1333
+ }
1334
+
1335
+ // ===== Agent 私有记忆策展(本人或 lead 写/删;kind='semantic' 人工策展)=====
1336
+
1337
+ /** 写/更新 Agent 私有记忆(caller 须为本人或 lead;稳定 dedupKey 幂等刷新)。
1338
+ * scope='shared'(caller 任意成员)→ 落 Channel 公共域(agent:<caller>:<key> 命名空间,全员可检索)。 */
1339
+ addAgentMemory(channelId: string, callerAgentId: string, targetAgentId: string, input: { title: string, content: string, importance?: number, dedupKey?: string, scope?: 'private' | 'shared' }, opts: { byOwner?: boolean } = {}): void {
1340
+ // byOwner:人类 owner 策展任意成员记忆(本人/lead 校验旁路;curator 以目标成员入事件)
1341
+ if (!opts.byOwner) {
1342
+ const caller = this.deps.repos.channelAgents.findByChannelAgent(channelId, callerAgentId)
1343
+ if (!caller) throw new AppError(403, 'SCOPE_VIOLATION', '调用方 Agent 不在本 channel')
1344
+ if (input.scope !== 'shared' && callerAgentId !== targetAgentId && caller.role !== 'lead') {
1345
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅本人或 lead 可策展 Agent 记忆')
1346
+ }
1347
+ }
1348
+ if (input.scope === 'shared') {
1349
+ // 共享域:走 AgentMemory.save 同源路径(命名空间 dedup + 自动向量化)
1350
+ const mem = new AgentMemory(this.deps.repos.memories, { channelId, agentId: callerAgentId, embedder: this.memoryEmbedder ?? undefined })
1351
+ void mem.save({
1352
+ title: input.title,
1353
+ content: input.content,
1354
+ importance: input.importance,
1355
+ scope: 'shared',
1356
+ dedupKey: input.dedupKey,
1357
+ }).then(saved => this.buses.get(channelId)?.notifyMemory({ agentId: callerAgentId, scope: 'shared', title: input.title, dedupKey: saved.dedupKey })).catch(() => {})
1358
+ return
1359
+ }
1360
+ if (!this.deps.repos.channelAgents.findByChannelAgent(channelId, targetAgentId)) {
1361
+ throw new AppError(404, 'NOT_FOUND', `Agent 实例不存在: ${targetAgentId}`)
1362
+ }
1363
+ const dedupKey = input.dedupKey ?? `manual:${randomUUID()}`
1364
+ this.deps.repos.memories.upsert({
1365
+ channelId,
1366
+ agentId: targetAgentId,
1367
+ kind: 'semantic',
1368
+ title: input.title,
1369
+ titleFts: segmentCJK(input.title),
1370
+ content: segmentCJK(input.content).slice(0, 800),
1371
+ importance: input.importance ?? 0.9,
1372
+ taskId: null,
1373
+ dedupKey,
1374
+ })
1375
+ // 策展行同样入向量域(未切分原文;provider 未配置/失败 → 静默留 FTS)
1376
+ void vectorizeMemory(this.deps.repos.memories, this.memoryEmbedder, channelId, targetAgentId, dedupKey, input.content).catch(() => {})
1377
+ this.buses.get(channelId)?.notifyMemory({ agentId: targetAgentId, scope: 'private', title: input.title, dedupKey })
1378
+ }
1379
+
1380
+ /** 删 Agent 私有记忆(caller 须为本人或 lead;行须属该 agent) */
1381
+ deleteAgentMemory(channelId: string, callerAgentId: string, targetAgentId: string, memoryId: string, opts: { byOwner?: boolean } = {}): void {
1382
+ if (!opts.byOwner) {
1383
+ const caller = this.deps.repos.channelAgents.findByChannelAgent(channelId, callerAgentId)
1384
+ if (!caller || (callerAgentId !== targetAgentId && caller.role !== 'lead')) {
1385
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅本人或 lead 可删除 Agent 记忆')
1386
+ }
1387
+ }
1388
+ const row = this.deps.repos.memories.listByAgent(targetAgentId, 10_000)
1389
+ .find(r => r.id === memoryId && r.channelId === channelId && r.agentId === targetAgentId)
1390
+ if (!row) throw new AppError(404, 'NOT_FOUND', `记忆不存在: ${memoryId}`)
1391
+ this.deps.repos.memories.delete(memoryId)
1392
+ }
1393
+
1394
+ /** 实例详情(含运行时装配状态) */
1395
+ getChannelAgent(instanceId: string): (AgentInfo & { wired: boolean, runtimeState: 'idle' | 'busy' | 'stopped' | null }) | undefined {
1396
+ const m = this.deps.repos.channelAgents.findById(instanceId)
1397
+ if (!m) return undefined
1398
+ const rt = this.runtimeOf(m.channelId, instanceId)
1399
+ return {
1400
+ ...instanceToAgentInfo(m),
1401
+ wired: rt !== undefined,
1402
+ runtimeState: rt ? rt.getState() : null,
1403
+ }
1404
+ }
1405
+
1406
+ /** 更新实例(name/harness/config/enabled);变更后卸载已装配运行时以重载 */
1407
+ async updateChannelAgent(
1408
+ instanceId: string,
1409
+ patch: { name?: string, harness?: string, config?: Record<string, unknown>, enabled?: number },
1410
+ meta?: { channelId?: string, by?: string, reason?: string },
1411
+ ): Promise<AgentInfo> {
1412
+ const m = this.deps.repos.channelAgents.findById(instanceId)
1413
+ if (!m) throw new AppError(404, 'NOT_FOUND', `实例不存在: ${instanceId}`)
1414
+ const updated = this.deps.repos.channelAgents.update(instanceId, patch)
1415
+ if (!updated) throw new AppError(404, 'NOT_FOUND', `实例不存在: ${instanceId}`)
1416
+ await this.unloadAgent(updated.channelId, instanceId)
1417
+ this.notifyMember(updated.channelId, {
1418
+ op: 'updated',
1419
+ agentId: instanceId,
1420
+ name: updated.name,
1421
+ role: updated.role as 'lead' | 'worker',
1422
+ harness: updated.harness,
1423
+ enabled: updated.enabled,
1424
+ config: parseJson<Record<string, unknown>>(updated.configJson, {}),
1425
+ by: meta?.by ?? 'user',
1426
+ reason: meta?.reason,
1427
+ })
1428
+ return instanceToAgentInfo(updated)
1429
+ }
1430
+
1431
+ /** 从 channel 移除实例(仅删实例,不删模板) */
1432
+ async removeAgentFromChannel(channelId: string, instanceId: string, meta?: { by?: string, reason?: string }): Promise<void> {
1433
+ const m = this.deps.repos.channelAgents.findByChannelAgent(channelId, instanceId)
1434
+ if (!m) return
1435
+ await this.stopAndDetach(channelId, instanceId)
1436
+ if (m.role === 'lead') {
1437
+ this.deps.repos.channels.update(channelId, { leadAgentId: null })
1438
+ }
1439
+ this.deps.repos.subscriptions.removeByAgent(channelId, instanceId)
1440
+ this.deps.repos.channelAgents.remove(channelId, instanceId)
1441
+ this.notifyMember(channelId, {
1442
+ op: 'removed',
1443
+ agentId: instanceId,
1444
+ name: m.name,
1445
+ role: m.role as 'lead' | 'worker',
1446
+ harness: m.harness,
1447
+ enabled: m.enabled,
1448
+ by: meta?.by ?? 'user',
1449
+ reason: meta?.reason,
1450
+ })
1451
+ }
1452
+
1453
+ // ===== Lead 自主团队管理面(执行中扩容/调参/裁撤;工具桥 + 调度决策共用) =====
1454
+
1455
+ /** lead 在本 channel 新建团队成员(worker):按需落模板(owner=channel 属主)并克隆为独立实例 */
1456
+ async createTeamMember(
1457
+ channelId: string,
1458
+ callerAgentId: string,
1459
+ input: { name: string, harness?: string, config?: Record<string, unknown>, templateId?: string, reason?: string },
1460
+ ): Promise<AgentInfo> {
1461
+ const caller = this.requireMember(channelId, callerAgentId)
1462
+ if (caller.role !== 'lead') throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可管理团队成员')
1463
+ const channel = this.deps.repos.channels.findById(channelId)
1464
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
1465
+ const name = input.name.trim()
1466
+ if (!name) throw new AppError(400, 'BAD_REQUEST', '成员名不能为空')
1467
+ let templateId = input.templateId
1468
+ if (templateId) {
1469
+ if (!this.deps.repos.agents.findById(templateId)) {
1470
+ throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${templateId}`)
1471
+ }
1472
+ }
1473
+ else {
1474
+ const harness = input.harness ?? 'omp'
1475
+ if (!KNOWN_HARNESSES.has(harness)) {
1476
+ throw new AppError(400, 'UNKNOWN_HARNESS', `未知 harness: ${harness}(可选 ${[...KNOWN_HARNESSES].join('/')})`)
1477
+ }
1478
+ const tpl = this.deps.repos.agents.create({
1479
+ name,
1480
+ harness,
1481
+ config: input.config,
1482
+ ownerUserId: channel.ownerUserId ?? null,
1483
+ })
1484
+ templateId = tpl.id
1485
+ }
1486
+ // 新成员立即装配:lead 建员即进入 channel 运行时(前端/调度立即可见,
1487
+ // 无需等首次任务投递的懒加载;DB 已由 addAgentToChannel 同步落库)
1488
+ const member = await this.addAgentToChannel({
1489
+ channelId,
1490
+ agentId: templateId,
1491
+ role: 'worker',
1492
+ by: `lead:${callerAgentId}`,
1493
+ reason: input.reason,
1494
+ })
1495
+ this.ensureAgentRuntime(channelId, member.id)
1496
+ return member
1497
+ }
1498
+
1499
+ /** lead 更新团队成员(改名/改配置/启停;不能改自己);变更后卸载运行时,下次消费按新配置重载 */
1500
+ async updateTeamMember(
1501
+ channelId: string,
1502
+ callerAgentId: string,
1503
+ agentId: string,
1504
+ patch: { name?: string, config?: Record<string, unknown>, enabled?: number, reason?: string },
1505
+ ): Promise<AgentInfo> {
1506
+ const caller = this.requireMember(channelId, callerAgentId)
1507
+ if (caller.role !== 'lead') throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可管理团队成员')
1508
+ if (agentId === callerAgentId) {
1509
+ throw new AppError(400, 'BAD_REQUEST', 'lead 不能在执行中更新自己(避免自毁调度循环)')
1510
+ }
1511
+ const target = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
1512
+ if (!target) throw new AppError(404, 'NOT_FOUND', `团队成员不存在: ${agentId}`)
1513
+ return this.updateChannelAgent(agentId, patch, { channelId, by: `lead:${callerAgentId}`, reason: patch.reason })
1514
+ }
1515
+
1516
+ /**
1517
+ * lead 移除团队成员(worker;不能移除自己)。
1518
+ * 孤儿任务回收(成员移除后其任务不能悬死):
1519
+ * - SUBMITTED/ASSIGNED(排队中)→ 重派给剩余队列最短的 worker;无接收者 → 取消
1520
+ * - WORKING/WAITING(执行中)→ 中止运行时并置 FAILED,交调度循环按重试策略重派
1521
+ */
1522
+ async removeTeamMember(
1523
+ channelId: string,
1524
+ callerAgentId: string,
1525
+ agentId: string,
1526
+ reason?: string,
1527
+ ): Promise<{ recycledTasks: string[] }> {
1528
+ const caller = this.requireMember(channelId, callerAgentId)
1529
+ if (caller.role !== 'lead') throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可管理团队成员')
1530
+ if (agentId === callerAgentId) {
1531
+ throw new AppError(400, 'BAD_REQUEST', 'lead 不能移除自己(移除 lead 请用 REST 删除)')
1532
+ }
1533
+ const target = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
1534
+ if (!target) throw new AppError(404, 'NOT_FOUND', `团队成员不存在: ${agentId}`)
1535
+
1536
+ const recycledTasks: string[] = []
1537
+ const orphans = this.deps.repos.tasks
1538
+ .listByChannelAssignee(channelId, agentId)
1539
+ .map(rowToTask)
1540
+ .filter(t => !TERMINAL_TASK_STATES[t.state])
1541
+ for (const task of orphans) {
1542
+ if (task.state === 'SUBMITTED' || task.state === 'ASSIGNED') {
1543
+ const receiver = this.pickReceiverWorker(channelId, agentId)
1544
+ if (receiver) {
1545
+ this.getTaskEngine().reassign(task.id, receiver)
1546
+ this.wakeAgent(channelId, receiver)
1547
+ }
1548
+ else {
1549
+ this.getTaskEngine().cancel(task.id, callerAgentId)
1550
+ }
1551
+ }
1552
+ else {
1553
+ // WORKING/WAITING:先中止在跑回合,再走 FAILED(调度循环 retry/reassign 兜底)
1554
+ this.runtimeOf(channelId, agentId)?.abortCurrent()
1555
+ if (task.state === 'WAITING') {
1556
+ this.getTaskEngine().transition(task.id, 'WORKING', callerAgentId)
1557
+ }
1558
+ this.getTaskEngine().transition(task.id, 'FAILED', callerAgentId)
1559
+ }
1560
+ recycledTasks.push(task.id)
1561
+ }
1562
+
1563
+ await this.removeAgentFromChannel(channelId, agentId, { by: `lead:${callerAgentId}`, reason })
1564
+ // lead 现场创建的一次性模板:无任何 channel 实例、未被编组引用 → 连模板一并删除
1565
+ // (数据库信息彻底清理;有外部引用则保留模板,仅删实例行)
1566
+ if (target.templateId) {
1567
+ const instances = this.deps.repos.channelAgents.listByTemplate(target.templateId)
1568
+ const teamRefs = this.deps.repos.teamMembers.listByTemplate(target.templateId)
1569
+ if (instances.length === 0 && teamRefs.length === 0) {
1570
+ this.deps.repos.agents.remove(target.templateId)
1571
+ }
1572
+ }
1573
+ return { recycledTasks }
1574
+ }
1575
+
1576
+ /** 剩余可用 worker 里选队列最短者(成员移除重派接收者) */
1577
+ private pickReceiverWorker(channelId: string, excludeAgentId: string): string | null {
1578
+ const candidates = this.deps.repos.channelAgents
1579
+ .listByChannel(channelId)
1580
+ .filter(m => m.enabled === 1 && m.role === 'worker' && m.id !== excludeAgentId)
1581
+ let best: string | null = null
1582
+ let bestLen = Number.POSITIVE_INFINITY
1583
+ for (const m of candidates) {
1584
+ const len = this.getTaskEngine().queueViewOf(channelId, m.id).queued.length
1585
+ if (len < bestLen) {
1586
+ best = m.id
1587
+ bestLen = len
1588
+ }
1589
+ }
1590
+ return best
1591
+ }
1592
+
1593
+ // ===== AgentTeam 管理面(模板编组 + 批量部署;v10 可见性隔离) =====
1594
+
1595
+ /** 创建 AgentTeam */
1596
+ async createTeam(input: { name: string, description?: string, visibility?: string, ownerUserId?: string | null }): Promise<AgentTeamDetail> {
1597
+ const row = this.deps.repos.teams.create({ name: input.name, description: input.description, visibility: input.visibility, ownerUserId: input.ownerUserId ?? null })
1598
+ return this.teamDetailOf(row)
1599
+ }
1600
+
1601
+ /** 全部 AgentTeam */
1602
+ async listTeams(): Promise<AgentTeamDetail[]> {
1603
+ return this.deps.repos.teams.list().map(row => this.teamDetailOf(row))
1604
+ }
1605
+
1606
+ /** 用户视角编组列表(本人 + 遗留公共) */
1607
+ async listTeamsForUser(userId: string): Promise<AgentTeamDetail[]> {
1608
+ return this.deps.repos.teams.listForOwner(userId).map(row => this.teamDetailOf(row))
1609
+ }
1610
+
1611
+ /** 可见性感知列表:普通用户 = 本人(任意可见性)+ 全部 public(含内置);admin = 全量 */
1612
+ async listTeamsVisibleTo(user: ActingUser): Promise<AgentTeamDetail[]> {
1613
+ const rows = user.role === 'admin'
1614
+ ? this.deps.repos.teams.list()
1615
+ : this.deps.repos.teams.listVisible(user.id)
1616
+ return rows.map(row => this.teamDetailOf(row))
1617
+ }
1618
+
1619
+ /** AgentTeam 详情(含成员模板快照);不存在返回 undefined */
1620
+ getTeam(teamId: string): AgentTeamDetail | undefined {
1621
+ const row = this.deps.repos.teams.findById(teamId)
1622
+ if (!row) return undefined
1623
+ return this.teamDetailOf(row)
1624
+ }
1625
+
1626
+ /** 更新 AgentTeam(name/description/visibility) */
1627
+ async updateTeam(teamId: string, patch: { name?: string, description?: string, visibility?: string }): Promise<AgentTeamDetail> {
1628
+ const updated = this.deps.repos.teams.update(teamId, patch)
1629
+ if (!updated) throw new AppError(404, 'NOT_FOUND', `AgentTeam 不存在: ${teamId}`)
1630
+ return this.teamDetailOf(updated)
1631
+ }
1632
+
1633
+ /** 删除 AgentTeam(仅删编组关系,不动模板与其已部署实例) */
1634
+ async removeTeam(teamId: string): Promise<void> {
1635
+ this.deps.repos.teams.remove(teamId)
1636
+ }
1637
+
1638
+ /** 把 Agent 模板加入 AgentTeam(同 team 内模板唯一;至多一个 lead) */
1639
+ async addTemplateToTeam(input: { teamId: string, templateId: string, role?: 'lead' | 'worker' }): Promise<AgentTeamDetail> {
1640
+ const team = this.deps.repos.teams.findById(input.teamId)
1641
+ if (!team) throw new AppError(404, 'NOT_FOUND', `AgentTeam 不存在: ${input.teamId}`)
1642
+ const tpl = this.deps.repos.agents.findById(input.templateId)
1643
+ if (!tpl) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${input.templateId}`)
1644
+ const role = input.role ?? 'worker'
1645
+ if (this.deps.repos.teamMembers.findByTeamTemplate(input.teamId, input.templateId)) {
1646
+ throw new AppError(409, 'ALREADY_MEMBER', `模板 ${input.templateId} 已在 team ${input.teamId} 中`)
1647
+ }
1648
+ if (role === 'lead' && this.deps.repos.teamMembers.countLead(input.teamId) > 0) {
1649
+ throw new AppError(409, 'LEAD_EXISTS', `team ${input.teamId} 已存在 lead`)
1650
+ }
1651
+ this.deps.repos.teamMembers.add({ teamId: input.teamId, templateId: input.templateId, role })
1652
+ return this.teamDetailOf(team)
1653
+ }
1654
+
1655
+ /** 从 AgentTeam 移除 Agent 模板(仅删编组关系) */
1656
+ async removeTemplateFromTeam(teamId: string, templateId: string): Promise<AgentTeamDetail> {
1657
+ const team = this.deps.repos.teams.findById(teamId)
1658
+ if (!team) throw new AppError(404, 'NOT_FOUND', `AgentTeam 不存在: ${teamId}`)
1659
+ const member = this.deps.repos.teamMembers.findByTeamTemplate(teamId, templateId)
1660
+ if (!member) throw new AppError(404, 'NOT_FOUND', `模板 ${templateId} 不在 team ${teamId} 中`)
1661
+ this.deps.repos.teamMembers.remove(teamId, templateId)
1662
+ return this.teamDetailOf(team)
1663
+ }
1664
+
1665
+ /**
1666
+ * 批量部署 AgentTeam → Channel:对每个成员模板调用 addAgentToChannel 克隆出独立实例。
1667
+ * - channel 已有 lead 且 team 成员含 lead → 该成员 409 LEAD_EXISTS,默认整体失败(事务性语义)。
1668
+ * 实际上为简化:逐个克隆,失败时抛错(已克隆实例保留,调用方可 remove 重试)。
1669
+ * - 返回每个成员的部署结果(实例 AgentInfo)。
1670
+ */
1671
+ async deployTeamToChannel(input: {
1672
+ channelId: string
1673
+ teamId: string
1674
+ }): Promise<{ channelId: string, teamId: string, agents: AgentInfo[] }> {
1675
+ const channel = this.deps.repos.channels.findById(input.channelId)
1676
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${input.channelId}`)
1677
+ const team = this.deps.repos.teams.findById(input.teamId)
1678
+ if (!team) throw new AppError(404, 'NOT_FOUND', `AgentTeam 不存在: ${input.teamId}`)
1679
+ const members = this.deps.repos.teamMembers.listByTeam(input.teamId)
1680
+ if (members.length === 0) {
1681
+ throw new AppError(400, 'TEAM_EMPTY', `team ${input.teamId} 无成员,先添加 Agent 模板`)
1682
+ }
1683
+ const agents: AgentInfo[] = []
1684
+ for (const m of members) {
1685
+ const inst = await this.addAgentToChannel({
1686
+ channelId: input.channelId,
1687
+ agentId: m.templateId,
1688
+ role: m.role === 'lead' ? 'lead' : 'worker',
1689
+ })
1690
+ agents.push(inst)
1691
+ }
1692
+ return { channelId: input.channelId, teamId: input.teamId, agents }
1693
+ }
1694
+
1695
+ private teamDetailOf(row: TeamRow): AgentTeamDetail {
1696
+ const members = this.deps.repos.teamMembers.listByTeam(row.id)
1697
+ return {
1698
+ id: row.id,
1699
+ name: row.name,
1700
+ description: row.description,
1701
+ visibility: row.ownerUserId === null ? 'public' : row.visibility,
1702
+ isBuiltin: row.ownerUserId === null,
1703
+ ownerUserId: row.ownerUserId ?? null,
1704
+ members: members.map((m) => {
1705
+ const tpl = this.deps.repos.agents.findById(m.templateId)
1706
+ return {
1707
+ templateId: m.templateId,
1708
+ name: tpl?.name ?? '(deleted)',
1709
+ harness: tpl?.harness ?? '',
1710
+ role: m.role === 'lead' ? 'lead' : 'worker',
1711
+ addedAt: m.createdAt,
1712
+ }
1713
+ }),
1714
+ createdAt: row.createdAt,
1715
+ updatedAt: row.updatedAt,
1716
+ }
1717
+ }
1718
+
1719
+ // ===== Channel 模板管理面(v10:场景 + 工作目录 + 成员组合;实例化一键建 channel) =====
1720
+
1721
+ /** 可见性感知 Channel 模板列表:普通用户 = 本人 + public(含内置);admin = 全量 */
1722
+ listChannelTemplatesVisibleTo(user: ActingUser): ChannelTemplateDetail[] {
1723
+ const rows = user.role === 'admin'
1724
+ ? this.deps.repos.channelTemplates.list()
1725
+ : this.deps.repos.channelTemplates.listVisible(user.id)
1726
+ return rows.map(row => this.channelTemplateDetailOf(row))
1727
+ }
1728
+
1729
+ /** Channel 模板详情;不存在返回 undefined */
1730
+ getChannelTemplate(templateId: string): ChannelTemplateDetail | undefined {
1731
+ const row = this.deps.repos.channelTemplates.findById(templateId)
1732
+ if (!row) return undefined
1733
+ return this.channelTemplateDetailOf(row)
1734
+ }
1735
+
1736
+ /** 创建 Channel 模板 */
1737
+ createChannelTemplate(input: {
1738
+ name: string
1739
+ description?: string
1740
+ scenarioPrompt?: string
1741
+ workspace?: string
1742
+ lead?: { name: string, harness: string, config?: Record<string, unknown> } | null
1743
+ members?: ChannelTemplateMember[]
1744
+ visibility?: string
1745
+ ownerUserId: string
1746
+ }): ChannelTemplateDetail {
1747
+ this.assertHarness(input.lead?.harness)
1748
+ const row = this.deps.repos.channelTemplates.create({
1749
+ name: input.name,
1750
+ description: input.description,
1751
+ scenarioPrompt: input.scenarioPrompt,
1752
+ workspace: input.workspace,
1753
+ lead: input.lead ?? null,
1754
+ members: input.members ?? [],
1755
+ visibility: input.visibility,
1756
+ ownerUserId: input.ownerUserId,
1757
+ })
1758
+ return this.channelTemplateDetailOf(row)
1759
+ }
1760
+
1761
+ /** 从既有 Channel 实例捕获模板:场景 + 工作目录 + lead(内联)+ 成员(有模板引用则引用,否则内联快照) */
1762
+ createChannelTemplateFromChannel(channelId: string, input: { name: string, description?: string, visibility?: string }, ownerUserId: string): ChannelTemplateDetail {
1763
+ const channel = this.deps.repos.channels.findById(channelId)
1764
+ if (!channel) throw new AppError(404, 'NOT_FOUND', `channel 不存在: ${channelId}`)
1765
+ const members = this.deps.repos.channelAgents.listByChannel(channelId)
1766
+ .filter(m => m.id !== channel.leadAgentId)
1767
+ .map((m): ChannelTemplateMember => m.templateId
1768
+ ? { templateId: m.templateId, role: m.role === 'lead' ? 'lead' : 'worker' }
1769
+ : { inline: { name: m.name, harness: m.harness, config: parseJson<Record<string, unknown>>(m.configJson, {}) }, role: m.role === 'lead' ? 'lead' : 'worker' })
1770
+ const leadInst = channel.leadAgentId
1771
+ ? this.deps.repos.channelAgents.findById(channel.leadAgentId)
1772
+ : undefined
1773
+ const lead = leadInst
1774
+ ? {
1775
+ name: leadInst.name,
1776
+ harness: leadInst.harness,
1777
+ config: parseJson<Record<string, unknown>>(leadInst.configJson, {}),
1778
+ }
1779
+ : null
1780
+ const row = this.deps.repos.channelTemplates.create({
1781
+ name: input.name,
1782
+ description: input.description,
1783
+ scenarioPrompt: channel.scenarioPrompt,
1784
+ workspace: channel.workspace,
1785
+ lead,
1786
+ members,
1787
+ visibility: input.visibility,
1788
+ ownerUserId,
1789
+ })
1790
+ return this.channelTemplateDetailOf(row)
1791
+ }
1792
+
1793
+ /** 更新 Channel 模板(属主/admin;内置拒绝) */
1794
+ updateChannelTemplate(templateId: string, patch: Parameters<ChannelTemplateRepo['update']>[1]): ChannelTemplateDetail {
1795
+ const updated = this.deps.repos.channelTemplates.update(templateId, patch)
1796
+ if (!updated) throw new AppError(404, 'NOT_FOUND', `Channel 模板不存在: ${templateId}`)
1797
+ return this.channelTemplateDetailOf(updated)
1798
+ }
1799
+
1800
+ /** 删除 Channel 模板(属主/admin;内置拒绝) */
1801
+ removeChannelTemplate(templateId: string): void {
1802
+ this.deps.repos.channelTemplates.remove(templateId)
1803
+ }
1804
+
1805
+ /**
1806
+ * 实例化 Channel 模板 → 新 channel(属主 = 操作用户):
1807
+ * 场景/工作目录照搬;lead 内联创建;成员逐个克隆(引用模板时校验操作者可读)。
1808
+ * 返回 createChannel 同构结果 + 成员实例数。
1809
+ */
1810
+ async instantiateChannelTemplate(templateId: string, user: ActingUser, nameOverride?: string): Promise<{ channelId: string, workspace: string, agentCount: number, leadAgentId?: string }> {
1811
+ const tpl = this.deps.repos.channelTemplates.findById(templateId)
1812
+ if (!tpl) throw new AppError(404, 'NOT_FOUND', `Channel 模板不存在: ${templateId}`)
1813
+ this.requireTemplateReadable(tpl, user, 'Channel 模板')
1814
+ const lead = tpl.leadJson
1815
+ ? parseJson<{ name: string, harness: string, config?: Record<string, unknown> } | null>(tpl.leadJson, null)
1816
+ : null
1817
+ const created = await this.createChannel({
1818
+ name: (nameOverride && nameOverride.trim()) || tpl.name,
1819
+ description: tpl.description,
1820
+ scenarioPrompt: tpl.scenarioPrompt,
1821
+ workspace: tpl.workspace || undefined,
1822
+ leadAgent: lead ? { name: lead.name, harness: lead.harness, config: lead.config } : undefined,
1823
+ ownerUserId: user.id,
1824
+ })
1825
+ const members = parseJson<ChannelTemplateMember[]>(tpl.membersJson, [])
1826
+ let agentCount = lead ? 1 : 0
1827
+ for (const m of members) {
1828
+ if ('templateId' in m) {
1829
+ const tplRow = this.deps.repos.agents.findById(m.templateId)
1830
+ if (!tplRow) throw new AppError(404, 'NOT_FOUND', `成员 Agent 模板不存在: ${m.templateId}`)
1831
+ this.requireTemplateReadable(tplRow, user, '成员 Agent 模板')
1832
+ await this.addAgentToChannel({ channelId: created.channelId, agentId: m.templateId, role: m.role, by: 'template' })
1833
+ }
1834
+ else {
1835
+ this.assertHarness(m.inline.harness)
1836
+ const inlineTpl = await this.createAgent({ name: m.inline.name, harness: m.inline.harness, config: m.inline.config, ownerUserId: user.id })
1837
+ await this.addAgentToChannel({ channelId: created.channelId, agentId: inlineTpl.id, role: m.role, by: 'template' })
1838
+ }
1839
+ agentCount += 1
1840
+ }
1841
+ return { channelId: created.channelId, workspace: created.workspace, agentCount, leadAgentId: created.leadAgentId }
1842
+ }
1843
+
1844
+ private assertHarness(harness: string | undefined): void {
1845
+ if (harness && !KNOWN_HARNESSES.has(harness)) {
1846
+ throw new AppError(400, 'BAD_REQUEST', `未知 harness: ${harness}`)
1847
+ }
1848
+ }
1849
+
1850
+ private channelTemplateDetailOf(row: ChannelTemplateRow): ChannelTemplateDetail {
1851
+ return {
1852
+ id: row.id,
1853
+ name: row.name,
1854
+ description: row.description,
1855
+ scenarioPrompt: row.scenarioPrompt,
1856
+ workspace: row.workspace,
1857
+ lead: row.leadJson ? parseJson<ChannelTemplateDetail['lead']>(row.leadJson, null) : null,
1858
+ members: parseJson<ChannelTemplateMember[]>(row.membersJson, []),
1859
+ visibility: row.ownerUserId === null ? 'public' : row.visibility,
1860
+ isBuiltin: row.ownerUserId === null,
1861
+ ownerUserId: row.ownerUserId ?? null,
1862
+ createdAt: row.createdAt,
1863
+ updatedAt: row.updatedAt,
1864
+ }
1865
+ }
1866
+
1867
+ // ===== 任务作业面 =====
1868
+
1869
+ /**
1870
+ * 人类向 channel 提交任务(HITL 控制面入口):
1871
+ * - 缺省 assigneeId → 自动路由 lead(原行为);
1872
+ * - 指定 assigneeId(Composer @ 某成员)→ 直发该成员:任务 create→ASSIGNED,
1873
+ * assign 消息直投其信箱(不经 lead 调度,人类即此刻的调度者);
1874
+ * - fromLabel(登录用户名)盖章 x-aw-from-label → 时间线以"用户章"渲染发送者。
1875
+ */
1876
+ async submitChannelTask(input: {
1877
+ channelId: string
1878
+ title: string
1879
+ description?: string
1880
+ parts?: Part[]
1881
+ mode?: ExecutionMode
1882
+ modeConfig?: ModeConfig
1883
+ /** HITL 直发目标(缺省 lead);须为本 channel 启用成员 */
1884
+ assigneeId?: string
1885
+ /** 人类发送者显示名(时间线归属) */
1886
+ fromLabel?: string
1887
+ }): Promise<WorkspaceTask> {
1888
+ const channel = this.deps.repos.channels.findById(input.channelId)
1889
+ if (!channel || !channel.leadAgentId) {
1890
+ throw new AppError(400, 'NO_LEAD_AGENT', `channel ${input.channelId} 无 lead,请先创建 lead`)
1891
+ }
1892
+ if (channel.enabled !== 1) {
1893
+ throw new AppError(403, 'CHANNEL_DISABLED', `channel ${input.channelId} 已禁用`)
1894
+ }
1895
+ this.ensureChannelActive(input.channelId)
1896
+ // 入口幂等(数据状态驱动):同 channel 同标题且**非终态**的任务已存在 → 409。
1897
+ // 挡住 HITL 双击/客户端重试造成的重复执行;前一轮已 COMPLETED 的 loop 重放
1898
+ // (LoopController → 本方法)不受影响 —— 终态不参与判定
1899
+ const duplicate = this.getTaskEngine().list(input.channelId).find(t =>
1900
+ t.title === input.title
1901
+ && t.state !== 'COMPLETED' && t.state !== 'FAILED' && t.state !== 'CANCELED')
1902
+ if (duplicate) {
1903
+ throw new AppError(409, 'TASK_DUPLICATE', `同标题任务已在途:「${input.title}」(${duplicate.id.slice(0, 8)},${duplicate.state}),请等待其完成或取消后再提交`)
1904
+ }
1905
+ // HITL 直发目标校验 + 容错寻址(id/名字/唯一前缀;人类输入名字即可直发)
1906
+ let assigneeId = channel.leadAgentId
1907
+ if (input.assigneeId && input.assigneeId !== channel.leadAgentId) {
1908
+ assigneeId = this.resolveMemberRef(input.channelId, input.assigneeId).id
1909
+ }
1910
+ const description = input.mode
1911
+ ? encodeTaskMode(input.mode, input.modeConfig ?? {}, input.description ?? '')
1912
+ : input.description
1913
+ let task = this.getTaskEngine().create({
1914
+ channelId: input.channelId,
1915
+ creatorId: '',
1916
+ assigneeId,
1917
+ title: input.title,
1918
+ description,
1919
+ parts: input.parts,
1920
+ })
1921
+ // 直发 worker:补 ASSIGNED 迁移(与 dispatchTask 独立任务同构,worker 消费循环按 assign 起回合)
1922
+ if (assigneeId !== channel.leadAgentId) {
1923
+ task = this.getTaskEngine().transition(task.id, 'ASSIGNED', 'system')
1924
+ }
1925
+ const messageMetadata: Record<string, unknown> = {
1926
+ 'x-aw-task-kind': 'assign',
1927
+ 'x-aw-task-id': task.id,
1928
+ }
1929
+ if (input.fromLabel) messageMetadata['x-aw-from-label'] = input.fromLabel
1930
+ const message = buildMessage(input.channelId, 'ROLE_USER', input.parts ?? [], messageMetadata)
1931
+ message.taskId = task.id
1932
+ const delivered = this.route(input.channelId, message)
1933
+ if (!delivered.includes(assigneeId)) {
1934
+ // 投递失败补偿:任务回收终态(不留幽灵任务,用户重试不再累积重复任务)
1935
+ this.getTaskEngine().transition(task.id, 'CANCELED', 'system')
1936
+ throw new AppError(502, 'DELIVERY_FAILED', `任务指派未能投递到 ${assigneeId.slice(0, 8)} 的信箱,请重试`)
1937
+ }
1938
+ if (assigneeId !== channel.leadAgentId) this.wakeAgent(input.channelId, assigneeId)
1939
+ this.ensureChannelRuntime(input.channelId).wakeScheduler()
1940
+ return task
1941
+ }
1942
+
1943
+ async dispatchTask(
1944
+ channelId: string,
1945
+ callerAgentId: string,
1946
+ input: {
1947
+ parentTaskId?: string
1948
+ assigneeId: string
1949
+ title: string
1950
+ description?: string
1951
+ parts?: Part[]
1952
+ routeReason?: string
1953
+ },
1954
+ ): Promise<WorkspaceTask> {
1955
+ const caller = this.requireMember(channelId, callerAgentId)
1956
+ if (caller.role !== 'lead') {
1957
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可 dispatch 任务')
1958
+ }
1959
+ const assignee = this.deps.repos.channelAgents.findByChannelAgent(channelId, input.assigneeId)
1960
+ if (!assignee) {
1961
+ throw new AppError(403, 'SCOPE_VIOLATION', 'assignee 不在本 channel')
1962
+ }
1963
+ let task: WorkspaceTask
1964
+ if (input.parentTaskId) {
1965
+ const parent = this.getTaskEngine().get(input.parentTaskId)
1966
+ if (!parent) throw new AppError(404, 'NOT_FOUND', `父任务不存在: ${input.parentTaskId}`)
1967
+ if (parent.channelId !== channelId) {
1968
+ throw new AppError(403, 'SCOPE_VIOLATION', '父任务不在本 channel')
1969
+ }
1970
+ // 防重复派发守卫(确定性,不依赖 LLM 纪律):同父任务下同标题子任务
1971
+ // - 在途(非终态)→ 拒绝,告知等待现有执行(省 token 不重跑)
1972
+ // - 已完成且有交付 → 拒绝,直接附上既有成果预览(lead 可引用,不必重做)
1973
+ const norm = (s: string): string => s.replace(/\s+/g, ' ').trim().toLowerCase()
1974
+ const siblings = this.getTaskEngine().list(channelId).filter(t => t.parentId === input.parentTaskId)
1975
+ const dupTitle = (t: WorkspaceTask): boolean => norm(t.title) === norm(input.title)
1976
+ const inFlight = siblings.find(t => dupTitle(t) && !['COMPLETED', 'CANCELED', 'FAILED'].includes(t.state))
1977
+ if (inFlight) {
1978
+ throw new AppError(409, 'DUPLICATE_DISPATCH', `子任务 "${input.title}" 已在执行中(状态 ${inFlight.state},指派 ${inFlight.assigneeId?.slice(0, 8) ?? '?'})。不要重复派发——等待其完成;若内容有差异请换一个能区分意图的标题。`)
1979
+ }
1980
+ const done = siblings.find(t => dupTitle(t) && t.state === 'COMPLETED' && t.artifacts.length > 0)
1981
+ if (done) {
1982
+ const preview = done.artifacts[0]?.parts
1983
+ ?.map(p => ('text' in p ? p.text : ''))
1984
+ .join(' ')
1985
+ .replace(/\s+/g, ' ')
1986
+ .trim()
1987
+ .slice(0, 300) ?? ''
1988
+ throw new AppError(409, 'DUPLICATE_DISPATCH', `子任务 "${input.title}" 已完成并交付(任务 ${done.id})。已有成果:${preview || '(见任务详情)'}。直接引用该成果即可,不要重复派发相同工作。`)
1989
+ }
1990
+ task = this.getTaskEngine().dispatch(parent, {
1991
+ assigneeId: input.assigneeId,
1992
+ title: input.title,
1993
+ description: input.description,
1994
+ parts: input.parts,
1995
+ routeReason: input.routeReason,
1996
+ })
1997
+ this.wakeAgent(channelId, input.assigneeId)
1998
+ }
1999
+ else {
2000
+ task = this.getTaskEngine().create({
2001
+ channelId,
2002
+ creatorId: callerAgentId,
2003
+ assigneeId: input.assigneeId,
2004
+ title: input.title,
2005
+ description: input.description,
2006
+ parts: input.parts,
2007
+ })
2008
+ task = this.getTaskEngine().transition(task.id, 'ASSIGNED', callerAgentId)
2009
+ const message = buildMessage(channelId, 'ROLE_USER', input.parts ?? [], {
2010
+ 'x-aw-task-kind': 'assign',
2011
+ 'x-aw-task-id': task.id,
2012
+ 'x-aw-from-agent': callerAgentId,
2013
+ })
2014
+ message.taskId = task.id
2015
+ const delivered = this.route(channelId, message)
2016
+ if (!delivered.includes(input.assigneeId)) {
2017
+ // 投递失败补偿:子任务回收终态(父任务等全部子任务终态后由调度器重新决策)
2018
+ this.getTaskEngine().transition(task.id, 'CANCELED', callerAgentId)
2019
+ throw new AppError(502, 'DELIVERY_FAILED', `子任务指派未能投递到 ${input.assigneeId.slice(0, 8)} 的信箱,请重试或改派`)
2020
+ }
2021
+ }
2022
+ return task
2023
+ }
2024
+
2025
+ /**
2026
+ * 任务拒绝(assignee 认为任务超出自己能力/作业范畴时主动退回):
2027
+ * - 任务置 FAILED(调度器将按重试规则改派他人,而非本 Agent 硬扛或静默搁置);
2028
+ * - 回执拒绝通知:优先发任务创建者(本 channel 成员),否则 channel lead ——
2029
+ * 有发送人就必须让对方知道"为什么被拒",防止错发任务石沉大海。
2030
+ */
2031
+ async refuseTask(
2032
+ channelId: string,
2033
+ refuserId: string,
2034
+ taskId: string,
2035
+ reason: string,
2036
+ ): Promise<{ task: WorkspaceTask, notifiedTo: string | null }> {
2037
+ this.requireMember(channelId, refuserId)
2038
+ const engine = this.getTaskEngine()
2039
+ const task = engine.get(taskId)
2040
+ if (!task || task.channelId !== channelId) {
2041
+ throw new AppError(404, 'NOT_FOUND', `任务不存在或不在本 channel: ${taskId}`)
2042
+ }
2043
+ if (task.assigneeId !== refuserId) {
2044
+ throw new AppError(403, 'SCOPE_VIOLATION', `只能拒绝指派给自己的任务(assignee=${task.assigneeId.slice(0, 8)})`)
2045
+ }
2046
+ if (task.state === 'WAITING') {
2047
+ throw new AppError(400, 'INVALID_TRANSITION', 'WAITING 父任务(有子任务/阶段在执行)不可整体拒绝;请拒绝对应的子任务,父任务会按子任务结果汇总')
2048
+ }
2049
+ const trimmedReason = reason.trim()
2050
+ if (!trimmedReason) throw new AppError(400, 'BAD_REQUEST', '拒绝必须说明原因(reason)')
2051
+
2052
+ let current = task
2053
+ if (!TERMINAL_TASK_STATES[current.state]) {
2054
+ // 状态机需经 WORKING 到 FAILED(SUBMITTED/ASSIGNED 直达 FAILED 非法)
2055
+ if (current.state === 'SUBMITTED' || current.state === 'ASSIGNED') {
2056
+ current = engine.transition(taskId, 'WORKING', refuserId)
2057
+ }
2058
+ if (current.state !== 'FAILED') {
2059
+ current = engine.transition(taskId, 'FAILED', refuserId)
2060
+ }
2061
+ // 拒绝理由进任务历史(审计留痕)
2062
+ this.deps.repos.tasks.update(taskId, {
2063
+ history: [...current.history, {
2064
+ messageId: randomUUID(),
2065
+ contextId: channelId,
2066
+ role: 'ROLE_AGENT' as const,
2067
+ parts: [{ text: `[任务拒绝] ${refuserId.slice(0, 8)}:${trimmedReason}` }],
2068
+ }],
2069
+ })
2070
+ }
2071
+
2072
+ // 回执对象:任务创建者(仍为本 channel 在册成员)优先,fallback channel lead
2073
+ const members = this.deps.repos.channelAgents.listByChannel(channelId).filter(m => m.enabled === 1)
2074
+ const notifyTo = members.find(m => m.id === task.creatorId)?.id
2075
+ ?? members.find(m => m.role === 'lead')?.id
2076
+ ?? null
2077
+ if (notifyTo && notifyTo !== refuserId) {
2078
+ const message = buildMessage(channelId, 'ROLE_AGENT', [{
2079
+ text: `【任务拒绝回执】任务「${task.title}」(${taskId.slice(0, 8)}) 已被 ${refuserId.slice(0, 8)} 拒绝。原因:${trimmedReason}。请改派给能力匹配的成员,或取消该任务。`,
2080
+ }], {
2081
+ 'x-aw-target-agent': notifyTo,
2082
+ 'x-aw-from-agent': refuserId,
2083
+ 'x-aw-msg-priority': 'immediate',
2084
+ })
2085
+ const delivered = this.route(channelId, message)
2086
+ return { task: engine.get(taskId) ?? current, notifiedTo: delivered.includes(notifyTo) ? notifyTo : null }
2087
+ }
2088
+ return { task: engine.get(taskId) ?? current, notifiedTo: null }
2089
+ }
2090
+
2091
+ async reportTask(
2092
+ channelId: string,
2093
+ callerAgentId: string,
2094
+ input: { taskId: string, progress?: number, artifact?: A2AArtifact, message?: string },
2095
+ ): Promise<WorkspaceTask> {
2096
+ const task = this.requireTaskInScope(channelId, callerAgentId, input.taskId)
2097
+ if (task.assigneeId !== callerAgentId) {
2098
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 assignee 可上报任务')
2099
+ }
2100
+ // 终态设防:进度/交付物/历史是任务活性信号,终态任务(COMPLETED/FAILED/CANCELED)
2101
+ // 不得再写 —— 否则状态数据被污染(如 cancel 后迟到回合继续长出"新进度"),
2102
+ // 破坏"所有执行都标记 state"的数据状态驱动不变量
2103
+ if (task.state === 'COMPLETED' || task.state === 'FAILED' || task.state === 'CANCELED') {
2104
+ return task
2105
+ }
2106
+ const patch: TaskPatch = {}
2107
+ if (input.progress !== undefined) patch.progress = input.progress
2108
+ if (input.artifact) patch.artifacts = [...task.artifacts, input.artifact]
2109
+ if (input.message) {
2110
+ patch.history = [
2111
+ ...task.history,
2112
+ { messageId: randomUUID(), contextId: task.channelId, role: 'ROLE_AGENT' as const, parts: [{ text: input.message }] },
2113
+ ].slice(-200)
2114
+ }
2115
+ const updated = this.deps.repos.tasks.update(input.taskId, patch)
2116
+ const next = rowToTask(updated!)
2117
+ this.notifyTask(task.channelId, { taskId: task.id, progress: next.progress, agentId: callerAgentId })
2118
+ return next
2119
+ }
2120
+
2121
+ async completeTask(
2122
+ channelId: string,
2123
+ callerAgentId: string,
2124
+ input: { taskId: string, artifacts?: A2AArtifact[] },
2125
+ ): Promise<WorkspaceTask> {
2126
+ const task = this.requireTaskInScope(channelId, callerAgentId, input.taskId)
2127
+ if (task.assigneeId !== callerAgentId) {
2128
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 assignee 可完成任务')
2129
+ }
2130
+ // 终态幂等:已完成 → 原样返回(重复 complete 不炸);
2131
+ // 已取消/失败 → 语义化错误(非裸状态机异常),调用方可据此引导继续下一项
2132
+ if (task.state === 'COMPLETED') return task
2133
+ if (task.state === 'CANCELED' || task.state === 'FAILED') {
2134
+ throw new AppError(409, 'TASK_TERMINAL', `任务 ${input.taskId.slice(0, 8)} 已被平台${task.state === 'CANCELED' ? '取消' : '判定失败'},不能再标记完成;请继续处理队列下一项`)
2135
+ }
2136
+ const completed = this.getTaskEngine().complete(input.taskId, input.artifacts)
2137
+ // goal 保底合成产物广播(goal 模式父任务且 lead 未自带总结时,taskEngine 追加了
2138
+ // 平台合成的 goal-summary;以 goal-summary 语义 + artifactId 差集识别,幂等不重复广播)
2139
+ const knownIds = new Set([
2140
+ ...task.artifacts.map(a => a.artifactId),
2141
+ ...(input.artifacts ?? []).map(a => a.artifactId),
2142
+ ])
2143
+ for (const artifact of completed.artifacts) {
2144
+ if (isGoalSummaryArtifact(artifact) && !knownIds.has(artifact.artifactId)) {
2145
+ this.runtimeOf(channelId, callerAgentId)?.emitExternal({ kind: 'artifact', artifact }, callerAgentId)
2146
+ }
2147
+ }
2148
+ if (completed.parentId) {
2149
+ this.getTaskEngine().onChildCompleted(completed)
2150
+ const parent = this.getTaskEngine().get(completed.parentId)
2151
+ if (parent) this.wakeAgent(completed.channelId, parent.assigneeId)
2152
+ }
2153
+ return completed
2154
+ }
2155
+
2156
+ async cancelTask(
2157
+ channelId: string,
2158
+ callerAgentId: string,
2159
+ input: { taskId: string },
2160
+ ): Promise<WorkspaceTask> {
2161
+ const task = this.requireTaskInScope(channelId, callerAgentId, input.taskId)
2162
+ const caller = this.requireMember(channelId, callerAgentId)
2163
+ const isLead = caller.role === 'lead'
2164
+ const isCreator = task.creatorId === callerAgentId
2165
+ if (!isLead && !isCreator) {
2166
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead/creator 可取消任务')
2167
+ }
2168
+ const canceled = this.getTaskEngine().cancel(input.taskId, callerAgentId)
2169
+ this.runtimeOf(channelId, canceled.assigneeId)?.abortCurrent()
2170
+ this.wakeAgent(channelId, canceled.assigneeId)
2171
+ return canceled
2172
+ }
2173
+
2174
+ /**
2175
+ * HITL:前端独立中断指定成员运行时(worker 或 lead)。
2176
+ * - worker:强制 stop + detach(中断当前 run/杀子进程),成员行保留 enabled=1,
2177
+ * 后续任务投递按需重新装配(interrupt 语义,不删成员)。
2178
+ * - lead:stopAndDetach 内部同时停 SchedulerLoop;channel 恢复活跃由
2179
+ * 下次任务提交(ensureChannelActive)自动重装配 lead + 调度器。
2180
+ * 变更经 AEP agent.member(op=updated) 广播回流前端。
2181
+ */
2182
+ async stopAgentRuntime(channelId: string, agentId: string, by = 'user'): Promise<{ agentId: string, stopped: boolean }> {
2183
+ const m = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
2184
+ if (!m) throw new AppError(404, 'NOT_FOUND', `成员不存在: ${agentId}`)
2185
+ await this.stopAndDetach(channelId, agentId)
2186
+ this.notifyMember(channelId, {
2187
+ op: 'updated',
2188
+ agentId,
2189
+ name: m.name,
2190
+ role: m.role as 'lead' | 'worker',
2191
+ harness: m.harness,
2192
+ enabled: m.enabled,
2193
+ by,
2194
+ reason: 'HITL stop',
2195
+ })
2196
+ return { agentId, stopped: true }
2197
+ }
2198
+
2199
+ /**
2200
+ * HITL:用户重试 FAILED 任务(lead/worker 任务均可)。
2201
+ * 优先原 assignee(仍在本 channel 且启用),否则选队列最短的空闲 worker;
2202
+ * 无可用承接者 → 400 NO_WORKER。重试后经调度循环重新投递执行。
2203
+ */
2204
+ async retryTask(channelId: string, callerAgentId: string, taskId: string): Promise<WorkspaceTask> {
2205
+ const task = this.requireTaskInScope(channelId, callerAgentId, taskId)
2206
+ if (task.state !== 'FAILED') {
2207
+ throw new AppError(400, 'INVALID_STATE', `仅 FAILED 任务可重试(当前 ${task.state})`)
2208
+ }
2209
+ let target = this.deps.repos.channelAgents.findByChannelAgent(channelId, task.assigneeId)
2210
+ if (!target || target.enabled !== 1) {
2211
+ const receiver = this.pickReceiverWorker(channelId, '')
2212
+ if (!receiver) throw new AppError(400, 'NO_WORKER', '无可用 worker 承接重试任务')
2213
+ target = this.deps.repos.channelAgents.findByChannelAgent(channelId, receiver)!
2214
+ }
2215
+ const updated = this.getTaskEngine().reassign(taskId, target.id)
2216
+ this.wakeAgent(channelId, target.id)
2217
+ return updated
2218
+ }
2219
+
2220
+ async listTasks(channelId: string, callerAgentId: string): Promise<WorkspaceTask[]> {
2221
+ this.requireMember(channelId, callerAgentId)
2222
+ return this.deps.repos.tasks.listByChannel(channelId).map(rowToTask)
2223
+ }
2224
+
2225
+ async getTask(channelId: string, callerAgentId: string, taskId: string): Promise<WorkspaceTask> {
2226
+ return this.requireTaskInScope(channelId, callerAgentId, taskId)
2227
+ }
2228
+
2229
+ /** 自己的任务队列视图(待执行 FIFO / 执行中 / 已完成)——每个 agent 的任务管理系统入口 */
2230
+ async myQueue(channelId: string, callerAgentId: string): Promise<AgentTaskQueueView> {
2231
+ this.requireMember(channelId, callerAgentId)
2232
+ return this.getTaskEngine().queueViewOf(channelId, callerAgentId)
2233
+ }
2234
+
2235
+ /** 全员实时状态 + 队列总览(lead 统一调度/最优调配的观察面) */
2236
+ async queueOverview(channelId: string, callerAgentId: string): Promise<AgentStatusView[]> {
2237
+ this.requireMember(channelId, callerAgentId)
2238
+ const cr = this.channels.get(channelId)
2239
+ const wired = new Map((cr?.getAgents() ?? []).map(a => [a.agentId, a]))
2240
+ return this.deps.repos.channelAgents.listByChannel(channelId)
2241
+ .filter(m => m.enabled === 1)
2242
+ .map((m) => {
2243
+ const runtime = wired.get(m.id)
2244
+ if (runtime) return runtime.getStatus()
2245
+ // 未装配(懒加载)成员:状态按 idle,队列视图仍来自 tasks 表
2246
+ const view = this.getTaskEngine().queueViewOf(channelId, m.id)
2247
+ return {
2248
+ agentId: m.id,
2249
+ channelId,
2250
+ role: m.role as 'lead' | 'worker',
2251
+ name: m.name,
2252
+ state: 'idle' as const,
2253
+ currentTaskId: view.current?.id ?? null,
2254
+ currentTaskTitle: view.current?.title ?? null,
2255
+ currentTaskProgress: view.current?.progress != null ? view.current.progress : null,
2256
+ queuedCount: view.queued.length,
2257
+ completedCount: view.completed.length,
2258
+ }
2259
+ })
2260
+ }
2261
+
2262
+ /** 修改待执行任务(lead 对 worker 队列的"改";仅待执行态可改)+ 唤醒 assignee 消费新投递 */
2263
+ async updateTask(
2264
+ channelId: string,
2265
+ callerAgentId: string,
2266
+ taskId: string,
2267
+ patch: { title?: string, description?: string },
2268
+ ): Promise<WorkspaceTask> {
2269
+ const task = this.requireTaskInScope(channelId, callerAgentId, taskId)
2270
+ const caller = this.requireMember(channelId, callerAgentId)
2271
+ if (caller.role !== 'lead' && task.creatorId !== callerAgentId) {
2272
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead/创建者可修改任务')
2273
+ }
2274
+ const updated = this.getTaskEngine().updateTask(taskId, patch, callerAgentId)
2275
+ this.wakeAgent(channelId, updated.assigneeId)
2276
+ return updated
2277
+ }
2278
+
2279
+ /** 重新指派(lead 的"调配":待执行/失败任务迁移到其他 worker)+ 唤醒新 assignee */
2280
+ async reassignTask(
2281
+ channelId: string,
2282
+ callerAgentId: string,
2283
+ taskId: string,
2284
+ toAgentId: string,
2285
+ ): Promise<WorkspaceTask> {
2286
+ this.requireTaskInScope(channelId, callerAgentId, taskId) // 校验任务在调用方作用域内
2287
+ const caller = this.requireMember(channelId, callerAgentId)
2288
+ if (caller.role !== 'lead') {
2289
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可调配任务')
2290
+ }
2291
+ const target = this.resolveMemberRef(channelId, toAgentId)
2292
+ const updated = this.getTaskEngine().reassign(taskId, target.id)
2293
+ this.wakeAgent(channelId, target.id)
2294
+ return updated
2295
+ }
2296
+
2297
+ async sendA2A(
2298
+ channelId: string,
2299
+ callerAgentId: string,
2300
+ input: { toAgentId: string, parts: Part[], metadata?: Record<string, unknown> },
2301
+ ): Promise<A2AMessage> {
2302
+ this.requireMember(channelId, callerAgentId)
2303
+ // 容错寻址:id/模板 id/名字/唯一前缀;失败错误携带当前名册(LLM 自我纠正)
2304
+ const target = this.resolveMemberRef(channelId, input.toAgentId)
2305
+ const message = buildMessage(channelId, 'ROLE_AGENT', input.parts, {
2306
+ ...(input.metadata ?? {}),
2307
+ 'x-aw-target-agent': target.id,
2308
+ 'x-aw-from-agent': callerAgentId,
2309
+ })
2310
+ // 真送达契约:route 回执必须包含目标(落库 enqueue 同步完成),否则向上抛错 ——
2311
+ // 消息绝不允许"报告已发送"却没进对方信箱
2312
+ const delivered = this.route(channelId, message)
2313
+ if (!delivered.includes(target.id)) {
2314
+ throw new AppError(502, 'DELIVERY_FAILED', `消息未能投递到 ${target.name} 的信箱(成员装配失败),请重试或改投其他成员`)
2315
+ }
2316
+ return message
2317
+ }
2318
+
2319
+ /**
2320
+ * 实时消息(外部/系统注入):priority=immediate → busy 时 steer 注入运行中的 omp 会话。
2321
+ * 触发器 requireReply=true → 接收方须回执(执行结果+所需内容,in_reply_to 关联)。
2322
+ */
2323
+ async sendImmediateMessage(input: {
2324
+ channelId: string
2325
+ fromAgentId?: string
2326
+ toAgentId: string
2327
+ parts: Part[]
2328
+ requireReply?: boolean
2329
+ /** 人类发送者显示名(REST 注入时的时间线归属;进入 x-aw-from-label 元数据) */
2330
+ fromLabel?: string
2331
+ }): Promise<A2AMessage> {
2332
+ // 无主消息拦截:发送方必须可追溯(agent 成员或人类显示名),否则拒绝发送
2333
+ const hasFromAgent = !!input.fromAgentId && this.deps.repos.channelAgents.findByChannelAgent(input.channelId, input.fromAgentId) != null
2334
+ if (!hasFromAgent && !input.fromLabel) {
2335
+ throw new AppError(400, 'NO_SENDER', '消息缺少可追溯发送人(fromAgentId 须为本 channel 成员,或提供 fromLabel 人类显示名)')
2336
+ }
2337
+ const metadata: Record<string, unknown> = {
2338
+ 'x-aw-target-agent': input.toAgentId,
2339
+ 'x-aw-msg-priority': 'immediate',
2340
+ }
2341
+ if (hasFromAgent) metadata['x-aw-from-agent'] = input.fromAgentId
2342
+ if (input.fromLabel) metadata['x-aw-from-label'] = input.fromLabel
2343
+ if (input.requireReply) metadata['x-aw-require-reply'] = 'true'
2344
+ const message = buildMessage(input.channelId, 'ROLE_AGENT', input.parts, metadata)
2345
+ const delivered = this.route(input.channelId, message)
2346
+ if (!delivered.includes(input.toAgentId)) {
2347
+ throw new AppError(502, 'DELIVERY_FAILED', `消息未能投递到 ${input.toAgentId} 的信箱,请重试`)
2348
+ }
2349
+ return message
2350
+ }
2351
+
2352
+ /** 阻塞长轮询(poll_messages):到信即时唤醒,250ms 兜底重查 */
2353
+ async waitMailbox(channelId: string, callerAgentId: string, limit: number, waitMs: number): Promise<A2AMessage[]> {
2354
+ this.requireMember(channelId, callerAgentId)
2355
+ const runtime = this.ensureAgentRuntime(channelId, callerAgentId)
2356
+ if (runtime) return runtime.waitPending(limit, waitMs)
2357
+ // 运行时未装配(极端时序):退化为一次性快照查询
2358
+ return this.pollMailbox(channelId, callerAgentId, limit)
2359
+ }
2360
+
2361
+ async pollMailbox(channelId: string, callerAgentId: string, limit = 100): Promise<A2AMessage[]> {
2362
+ this.requireMember(channelId, callerAgentId)
2363
+ return this.deps.repos.messages
2364
+ .listPendingByChannelAgent(channelId, callerAgentId)
2365
+ .slice(0, limit)
2366
+ .map(rowToMessage)
2367
+ }
2368
+
2369
+ /** 确认消费自己 mailbox 的协作消息(读即取;id 须属于 caller 的 pending 集) */
2370
+ ackMailbox(channelId: string, callerAgentId: string, messageIds: string[]): void {
2371
+ this.requireMember(channelId, callerAgentId)
2372
+ const pending = new Set(
2373
+ this.deps.repos.messages
2374
+ .listPendingByChannelAgent(channelId, callerAgentId)
2375
+ .map(r => r.id),
2376
+ )
2377
+ for (const id of messageIds) {
2378
+ // claim 守卫:与 steer 注入原子竞争(peek 与 ack 之间消息可能被认领),
2379
+ // 认领成功才标记消费——失败说明 steer 已注入运行中会话,不再重复投递
2380
+ if (pending.has(id) && this.deps.repos.messages.claim(id)) {
2381
+ this.deps.repos.messages.markConsumed(id)
2382
+ }
2383
+ }
2384
+ }
2385
+
2386
+ /**
2387
+ * (仅 lead)Channel 邮件全览:全部 agent 间消息(含已消费/任务投递),按时间倒序。
2388
+ * lead 调度观察面——worker 间的点对点通信(含结果回执)对 lead 可见,
2389
+ * 供派发前判断"该结果是否已由某 worker 经 mail 产出",避免重复派发浪费资源。
2390
+ * 可选 agentId 过滤参与方(from 或 to)。
2391
+ */
2392
+ async listChannelMail(
2393
+ channelId: string,
2394
+ callerAgentId: string,
2395
+ opts: { limit?: number, agentId?: string } = {},
2396
+ ): Promise<ChannelMail[]> {
2397
+ const caller = this.requireMember(channelId, callerAgentId)
2398
+ if (caller.role !== 'lead') {
2399
+ throw new AppError(403, 'SCOPE_VIOLATION', '仅 lead 可查看 Channel 全部邮件')
2400
+ }
2401
+ const limit = Math.max(1, Math.min(500, opts.limit ?? 200))
2402
+ const mails = this.deps.repos.messages
2403
+ .listRecentByChannel(channelId, limit)
2404
+ .map(rowToChannelMail)
2405
+ if (opts.agentId) {
2406
+ return mails.filter(m => m.fromAgentId === opts.agentId || m.toAgentId === opts.agentId)
2407
+ }
2408
+ return mails
2409
+ }
2410
+
2411
+ async subscribe(channelId: string, callerAgentId: string, input: { agentIds?: string[] }): Promise<void> {
2412
+ this.requireMember(channelId, callerAgentId)
2413
+ for (const targetId of input.agentIds ?? []) {
2414
+ const target = this.deps.repos.channelAgents.findByChannelAgent(channelId, targetId)
2415
+ if (!target) {
2416
+ throw new AppError(403, 'SCOPE_VIOLATION', `目标 Agent ${targetId} 不在本 channel`)
2417
+ }
2418
+ this.deps.repos.subscriptions.add(channelId, callerAgentId, targetId)
2419
+ }
2420
+ }
2421
+
2422
+ /** 实例级 token → 实例视图(AgentInfo) */
2423
+ findByToken(token: string): AgentInfo | undefined {
2424
+ const m = this.deps.repos.channelAgents.findByToken(token)
2425
+ return m ? instanceToAgentInfo(m) : undefined
2426
+ }
2427
+
2428
+ async restore(): Promise<void> {
2429
+ this.deps.repos.messages.resetConsuming()
2430
+ const nonTerminal = this.deps.repos.tasks.listNonTerminal()
2431
+ const activeChannelIds = new Set(nonTerminal.map(t => t.channelId))
2432
+ for (const channelId of activeChannelIds) {
2433
+ const channel = this.deps.repos.channels.findById(channelId)
2434
+ if (!channel || channel.enabled !== 1) continue
2435
+ this.ensureChannelActive(channelId)
2436
+ }
2437
+ // 断线重连:ASSIGNED/WORKING 的叶子任务(无子任务)若无 pending assign 投递
2438
+ // (消息已被消费但任务未完成——进程内 run 抛错、或崩溃落在消费后),
2439
+ // 无人会重新驱动它(调度循环只 dispatch lead 名下任务;停滞检测要 stallMs×2 才 cancel)。
2440
+ // 重投 assign + 唤醒 assignee,由 processMessage 的终态检查保证幂等(执行从头重放)。
2441
+ // 父任务(WAITING/有子任务)不重投:由调度循环按子任务进度汇总推进。
2442
+ // 父任务集合按 channel 全量任务计算(含已完成子任务):
2443
+ // 「子任务全部完成、父任务 WAITING 待汇总」的父任务不能误判为叶子而重投。
2444
+ const parentIds = new Set<string>()
2445
+ for (const channelId of activeChannelIds) {
2446
+ for (const t of this.deps.repos.tasks.listByChannel(channelId)) {
2447
+ if (t.parentId) parentIds.add(t.parentId)
2448
+ }
2449
+ }
2450
+ for (const task of nonTerminal) {
2451
+ if (task.state !== 'ASSIGNED' && task.state !== 'WORKING') continue
2452
+ if (parentIds.has(task.id)) continue
2453
+ if (this.deps.repos.messages.hasPendingAssign(task.id)) continue
2454
+ this.getTaskEngine().redeliverAssign(task.id)
2455
+ }
2456
+ // 唤醒有未消费消息的 agent:重启前 consuming 的 assign 消息已被 resetConsuming 重投为 pending,
2457
+ // 若无人唤醒,worker 的运行时(懒加载)不会装配,重投消息滞留 pending,任务永远无法恢复。
2458
+ // 任务状态保持原样(不重置为 ASSIGNED):调度循环按 SUBMITTED/WORKING 重新驱动 lead 任务,
2459
+ // worker 经消息重放从 ASSIGNED/WORKING 恢复执行;WAITING 父任务等子任务完成后正常汇总。
2460
+ for (const { channelId, toAgentId } of this.deps.repos.messages.listPendingTargets()) {
2461
+ this.wakeAgent(channelId, toAgentId)
2462
+ }
2463
+ }
2464
+
2465
+ // ===== 内部辅助 =====
2466
+
2467
+ private route(channelId: string, message: A2AMessage): string[] {
2468
+ // 总线通知(a2a.message)由 ChannelRuntime 构造时注入的 onRouted 回调统一发出
2469
+ // (见 ensureChannelRuntime):调度器直呼 runtime.route 的消息同样可见
2470
+ return this.ensureChannelRuntime(channelId).route(message)
2471
+ }
2472
+
2473
+ private wakeAgent(channelId: string, agentId: string): void {
2474
+ this.ensureAgentRuntime(channelId, agentId)?.wakeMailbox()
2475
+ }
2476
+
2477
+ /**
2478
+ * 成员寻址(A2A 身份命中率的核心):接受 id(实例/模板)、名字、唯一名字前缀。
2479
+ * 多智能体对话中 LLM 常以名字称呼同事,名册缓存也可能滞后于成员重建 ——
2480
+ * 精确 id 未命中时按名字/前缀解析,让"想发给谁"总能落到"谁"的实例上;
2481
+ * 解析失败时把当前名册(id | 名字)放进错误信息,LLM 下一轮即可自我纠正。
2482
+ */
2483
+ private resolveMemberRef(channelId: string, ref: string): ChannelAgentRow {
2484
+ const trimmed = ref.trim()
2485
+ if (!trimmed) throw new AppError(400, 'BAD_REQUEST', '缺少目标成员引用(to_agent_id/assignee_id)')
2486
+ const members = this.deps.repos.channelAgents.listByChannel(channelId).filter(m => m.enabled === 1)
2487
+ // ① 精确实例 id
2488
+ const byId = members.find(m => m.id === trimmed)
2489
+ if (byId) return byId
2490
+ // ② 模板 id(名册/记忆中的旧实例 id 复用同一模板 → 按模板命中当前实例)
2491
+ const byTemplate = members.find(m => m.templateId === trimmed)
2492
+ if (byTemplate) return byTemplate
2493
+ // ③ 名字精确(大小写不敏感;含"← 你"等标记已剥离)
2494
+ const lower = trimmed.toLowerCase()
2495
+ const byName = members.find(m => m.name.toLowerCase() === lower)
2496
+ if (byName) return byName
2497
+ // ④ 唯一名字前缀/包含(多候选 → 拒绝,防止误投)
2498
+ const partial = members.filter(m => m.name.toLowerCase().includes(lower))
2499
+ if (partial.length === 1) return partial[0]!
2500
+ const roster = members.map(m => `${m.id} | ${m.name}(${m.role})`).join('; ')
2501
+ throw new AppError(404, 'MEMBER_NOT_FOUND',
2502
+ `目标成员未命中: "${trimmed}"。当前 Channel 名册(用其中 id 或名字寻址): ${roster}`)
2503
+ }
2504
+
2505
+ /** 成员寻址(公开包装:REST/工具桥共用;语义同 resolveMemberRef) */
2506
+ resolveChannelMember(channelId: string, ref: string): { id: string, name: string, role: string } {
2507
+ const m = this.resolveMemberRef(channelId, ref)
2508
+ return { id: m.id, name: m.name, role: m.role }
2509
+ }
2510
+
2511
+ /** 校验调用方是本 channel 实例;返回实例行(含 role) */
2512
+ private requireMember(channelId: string, agentId: string): ChannelAgentRow {
2513
+ const m = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
2514
+ if (!m) throw new AppError(403, 'SCOPE_VIOLATION', '调用方 Agent 不在本 channel')
2515
+ return m
2516
+ }
2517
+
2518
+ private requireTaskInScope(channelId: string, callerAgentId: string, taskId: string): WorkspaceTask {
2519
+ this.requireMember(channelId, callerAgentId)
2520
+ const row = this.deps.repos.tasks.findById(taskId)
2521
+ if (!row) throw new AppError(404, 'NOT_FOUND', `任务不存在: ${taskId}`)
2522
+ if (row.channelId !== channelId) {
2523
+ throw new AppError(403, 'SCOPE_VIOLATION', '任务不在本 channel')
2524
+ }
2525
+ return rowToTask(row)
2526
+ }
2527
+ }
2528
+
2529
+ /** 工厂:创建 manager(测试与集成均可注入依赖) */
2530
+ export function createAgentChannelManager(deps: ManagerDeps): AgentChannelManager {
2531
+ return new AgentChannelManager(deps)
2532
+ }
2533
+
2534
+ let managerSingleton: AgentChannelManager | null = null
2535
+
2536
+ export function initWorkshopManager(deps: ManagerDeps): AgentChannelManager {
2537
+ managerSingleton = new AgentChannelManager(deps)
2538
+ return managerSingleton
2539
+ }
2540
+
2541
+ export function getWorkshopManagerOrNull(): AgentChannelManager | null {
2542
+ return managerSingleton
2543
+ }