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.
- package/.AgentWorkShop/prompts/host-tools.json +631 -0
- package/.AgentWorkShop/prompts/lead-supervise.md +42 -0
- package/.AgentWorkShop/prompts/mode-goal.md +26 -0
- package/.AgentWorkShop/prompts/mode-loop.md +10 -0
- package/.AgentWorkShop/prompts/mode-pipeline.md +16 -0
- package/.AgentWorkShop/prompts/peer-lead-roster.md +3 -0
- package/.AgentWorkShop/prompts/peer-message.md +14 -0
- package/.AgentWorkShop/prompts/peer-reply-optional.md +2 -0
- package/.AgentWorkShop/prompts/peer-reply-required.md +8 -0
- package/.AgentWorkShop/prompts/scenario-default.md +11 -0
- package/.AgentWorkShop/prompts/system-manual.md +18 -0
- package/.AgentWorkShop/prompts/team-roster.md +9 -0
- package/.AgentWorkShop/prompts/worker-workflow.md +19 -0
- package/.env.example +34 -0
- package/README-zh.md +424 -0
- package/README.md +424 -0
- package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.json +1 -0
- package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.mtg0vz68-49792-75b0709555.baseline/1459e6269990a5d229b9228c8b2f62ffaa7d350068380ae8b875fee0db45cca6.source +180 -0
- package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.mtg0vz68-49792-75b0709555.baseline/c2991e909c70c5c12b90a407c7adcf63384c51668a2b334e6e047fa675c268fe.source +305 -0
- package/app/.mimosa/hook-state/sess_05189059-e42d-4188-8911-4f7baabe009b.mtg0vz68-49792-75b0709555.baseline/e09129039d1e27e3fac24becc9bdafd2d1a7c8049bff24173df007be865f0165.source +264 -0
- package/app/.mimosa/hook-status/sess_05189059-e42d-4188-8911-4f7baabe009b-28ef7c75ea.json +14 -0
- package/app/app.vue +139 -0
- package/app/assets/css/main.css +1257 -0
- package/app/components/AppHeader.vue +675 -0
- package/app/components/AppSidebar.vue +286 -0
- package/app/components/AwChart.vue +44 -0
- package/app/components/AwLogo.vue +75 -0
- package/app/components/workshop/A2aRpcDebugger.vue +237 -0
- package/app/components/workshop/AgentInspectorDrawer.vue +424 -0
- package/app/components/workshop/AgentLanesView.vue +947 -0
- package/app/components/workshop/ArtifactCard.vue +120 -0
- package/app/components/workshop/AssetLibrary.vue +450 -0
- package/app/components/workshop/ChannelSessionList.vue +593 -0
- package/app/components/workshop/CommandPalette.vue +270 -0
- package/app/components/workshop/Composer.vue +731 -0
- package/app/components/workshop/DeviceTwinPanel.vue +607 -0
- package/app/components/workshop/EventBlock.vue +602 -0
- package/app/components/workshop/FileSelectorModal.vue +188 -0
- package/app/components/workshop/InspectorPanel.vue +265 -0
- package/app/components/workshop/LiveAgentBar.vue +362 -0
- package/app/components/workshop/MemoryPanel.vue +364 -0
- package/app/components/workshop/MentionHoverCard.vue +155 -0
- package/app/components/workshop/MultiChannelView.vue +189 -0
- package/app/components/workshop/PaneSplitter.vue +142 -0
- package/app/components/workshop/StatsPanel.vue +168 -0
- package/app/components/workshop/TaskBoardView.vue +636 -0
- package/app/components/workshop/TaskInspectorDrawer.vue +390 -0
- package/app/components/workshop/TranscriptTimeline.vue +545 -0
- package/app/components/workshop/blocks/ClusterArtifact.vue +77 -0
- package/app/components/workshop/blocks/ClusterError.vue +85 -0
- package/app/components/workshop/blocks/ClusterLife.vue +87 -0
- package/app/components/workshop/blocks/ClusterMember.vue +93 -0
- package/app/components/workshop/blocks/ClusterMemory.vue +84 -0
- package/app/components/workshop/blocks/ClusterOther.vue +67 -0
- package/app/components/workshop/blocks/ClusterRoute.vue +359 -0
- package/app/components/workshop/blocks/ClusterStatus.vue +147 -0
- package/app/components/workshop/blocks/ClusterStream.vue +325 -0
- package/app/components/workshop/blocks/ClusterTask.vue +151 -0
- package/app/components/workshop/blocks/ClusterTool.vue +158 -0
- package/app/components/workshop/lanes/LaneBlocks.vue +149 -0
- package/app/components/workshop/terminal/OmpTerminalPanel.vue +1010 -0
- package/app/components/workshop/town/ModelPreview3D.vue +276 -0
- package/app/components/workshop/town/TownScene.ts +1355 -0
- package/app/components/workshop/town/TownScene3D.ts +4533 -0
- package/app/components/workshop/town/TownView.vue +7071 -0
- package/app/composables/useCodeCopy.ts +30 -0
- package/app/composables/useHttp.ts +8 -0
- package/app/composables/useRouteMeta.ts +50 -0
- package/app/composables/useSiteConfig.ts +21 -0
- package/app/composables/workshop/apiClient.ts +95 -0
- package/app/composables/workshop/useCharacterAssets.ts +292 -0
- package/app/composables/workshop/useClusteredBlocks.ts +150 -0
- package/app/composables/workshop/useComposerBus.ts +16 -0
- package/app/composables/workshop/useDaqStream.ts +379 -0
- package/app/composables/workshop/useDcwStream.ts +300 -0
- package/app/composables/workshop/useDeviceTwins.ts +147 -0
- package/app/composables/workshop/useEventBlocks.ts +662 -0
- package/app/composables/workshop/useLocalTime.ts +33 -0
- package/app/composables/workshop/useOpsLog.ts +164 -0
- package/app/composables/workshop/useSceneLayouts.ts +166 -0
- package/app/composables/workshop/useTownBus.ts +47 -0
- package/app/composables/workshop/useVisibleInterval.ts +42 -0
- package/app/composables/workshop/useWorkshopApi.ts +224 -0
- package/app/composables/workshop/useWorkshopWs.ts +113 -0
- package/app/config/index.ts +66 -0
- package/app/config/schema.ts +68 -0
- package/app/error.vue +76 -0
- package/app/layouts/default.vue +100 -0
- package/app/layouts/town.vue +16 -0
- package/app/pages/daq/[id].vue +796 -0
- package/app/pages/daq/index.vue +2577 -0
- package/app/pages/dcw/[id].vue +2259 -0
- package/app/pages/dcw/index.vue +1079 -0
- package/app/pages/index.vue +830 -0
- package/app/pages/logs/index.vue +621 -0
- package/app/pages/monitor.vue +665 -0
- package/app/pages/settings.vue +757 -0
- package/app/pages/tokens.vue +610 -0
- package/app/pages/town.vue +145 -0
- package/app/pages/users.vue +257 -0
- package/app/pages/workshop/agents.vue +382 -0
- package/app/pages/workshop/channel-templates.vue +456 -0
- package/app/pages/workshop/index.vue +604 -0
- package/app/pages/workshop/teams.vue +448 -0
- package/app/pages/workshop/w/[wsId].vue +516 -0
- package/app/plugins/http.ts +71 -0
- package/app/plugins/locale-restore.client.ts +13 -0
- package/app/plugins/runtime-config.client.ts +65 -0
- package/app/plugins/session-restore.client.ts +24 -0
- package/app/plugins/theme.client.ts +15 -0
- package/app/stores/app.ts +32 -0
- package/app/stores/route-trail.ts +45 -0
- package/app/stores/runtime-config.ts +212 -0
- package/app/stores/workshop/connection.ts +184 -0
- package/app/stores/workshop/entities.ts +350 -0
- package/app/stores/workshop/events.ts +292 -0
- package/app/stores/workshop/user.ts +176 -0
- package/app/stores/workshop/workspaces.ts +133 -0
- package/bin/aw.mjs +13 -0
- package/cli/aw.mjs +184 -0
- package/cli/commands/build.mjs +33 -0
- package/cli/commands/config.mjs +157 -0
- package/cli/commands/dev.mjs +74 -0
- package/cli/commands/doctor.mjs +120 -0
- package/cli/commands/home.mjs +75 -0
- package/cli/commands/init.mjs +150 -0
- package/cli/commands/register.mjs +123 -0
- package/cli/commands/start.mjs +106 -0
- package/cli/commands/status.mjs +67 -0
- package/cli/commands/update.mjs +116 -0
- package/cli/commands/version.mjs +32 -0
- package/cli/core/args.mjs +95 -0
- package/cli/core/context.mjs +264 -0
- package/cli/core/errors.mjs +18 -0
- package/cli/core/help.mjs +68 -0
- package/cli/core/logger.mjs +40 -0
- package/cli/core/registry.mjs +111 -0
- package/config.yml +70 -0
- package/docker-compose.yml +34 -0
- package/docs/cli.md +220 -0
- package/i18n/dicts/_en.json +1097 -0
- package/i18n/dicts/_en2.json +300 -0
- package/i18n/dicts/_en3.json +102 -0
- package/i18n/dicts/_en4.json +115 -0
- package/i18n/dicts/_en5.json +33 -0
- package/i18n/dicts/a2aRpcDebugger.json +10 -0
- package/i18n/dicts/agentInspectorDrawer.json +25 -0
- package/i18n/dicts/agentLanesView.json +57 -0
- package/i18n/dicts/agents.json +27 -0
- package/i18n/dicts/appHeader.json +7 -0
- package/i18n/dicts/appSidebar.json +1 -0
- package/i18n/dicts/artifactCard.json +6 -0
- package/i18n/dicts/assetLibrary.json +18 -0
- package/i18n/dicts/awChart.json +1 -0
- package/i18n/dicts/awLogo.json +1 -0
- package/i18n/dicts/channelSessionList.json +45 -0
- package/i18n/dicts/clusterArtifact.json +4 -0
- package/i18n/dicts/clusterError.json +3 -0
- package/i18n/dicts/clusterLife.json +5 -0
- package/i18n/dicts/clusterMember.json +3 -0
- package/i18n/dicts/clusterMemory.json +3 -0
- package/i18n/dicts/clusterOther.json +3 -0
- package/i18n/dicts/clusterRoute.json +15 -0
- package/i18n/dicts/clusterStatus.json +3 -0
- package/i18n/dicts/clusterStream.json +3 -0
- package/i18n/dicts/clusterTask.json +10 -0
- package/i18n/dicts/clusterTool.json +3 -0
- package/i18n/dicts/commandPalette.json +16 -0
- package/i18n/dicts/composer.json +38 -0
- package/i18n/dicts/ctpl.json +32 -0
- package/i18n/dicts/daq.json +157 -0
- package/i18n/dicts/daqDetail.json +55 -0
- package/i18n/dicts/dcw.json +68 -0
- package/i18n/dicts/dcwDetail.json +199 -0
- package/i18n/dicts/deviceTwinPanel.json +12 -0
- package/i18n/dicts/eventBlock.json +8 -0
- package/i18n/dicts/fileSelectorModal.json +6 -0
- package/i18n/dicts/inspectorPanel.json +12 -0
- package/i18n/dicts/laneBlocks.json +6 -0
- package/i18n/dicts/liveAgentBar.json +8 -0
- package/i18n/dicts/logs.json +39 -0
- package/i18n/dicts/memoryPanel.json +20 -0
- package/i18n/dicts/mentionHoverCard.json +4 -0
- package/i18n/dicts/modelPreview3D.json +1 -0
- package/i18n/dicts/monitor.json +5 -0
- package/i18n/dicts/multiChannelView.json +13 -0
- package/i18n/dicts/ompTerminalPanel.json +7 -0
- package/i18n/dicts/paneSplitter.json +3 -0
- package/i18n/dicts/settings.json +1 -0
- package/i18n/dicts/statsPanel.json +6 -0
- package/i18n/dicts/taskBoardView.json +22 -0
- package/i18n/dicts/taskInspectorDrawer.json +19 -0
- package/i18n/dicts/teams.json +33 -0
- package/i18n/dicts/tokens.json +30 -0
- package/i18n/dicts/town.json +7 -0
- package/i18n/dicts/townView.json +209 -0
- package/i18n/dicts/transcriptTimeline.json +31 -0
- package/i18n/dicts/users.json +15 -0
- package/i18n/dicts/wsHome.json +33 -0
- package/i18n/dicts/wsView.json +15 -0
- package/i18n/locales/en.ts +1737 -0
- package/i18n/locales/zh-CN.ts +1736 -0
- package/nuxt.config.ts +209 -0
- package/package.json +123 -0
- package/public/assets/game/character/bot.png +0 -0
- package/public/assets/game/character/device-3d.glb +0 -0
- package/public/assets/game/character/hero-3d.glb +0 -0
- package/public/assets/game/character/hero-anime-1.glb +0 -0
- package/public/assets/game/character/hero-anime-2.glb +0 -0
- package/public/assets/game/character/hero-anime-3.glb +0 -0
- package/public/assets/game/character/hero-anime-4.glb +0 -0
- package/public/assets/game/character/knight.png +0 -0
- package/public/assets/game/character/mage.png +0 -0
- package/public/assets/game/coin.png +0 -0
- package/public/assets/game/devices/agv.glb +0 -0
- package/public/assets/game/devices/caster.glb +0 -0
- package/public/assets/game/devices/device-console.glb +0 -0
- package/public/assets/game/devices/device-robot-arm.glb +0 -0
- package/public/assets/game/devices/device-scanner.glb +0 -0
- package/public/assets/game/devices/extruder.glb +0 -0
- package/public/assets/game/devices/mdo.glb +0 -0
- package/public/assets/game/devices/power-cabinet.glb +0 -0
- package/public/assets/game/devices/pump.glb +0 -0
- package/public/assets/game/devices/tdo.glb +0 -0
- package/public/assets/game/devices/thickness-scanner.glb +0 -0
- package/public/assets/game/devices/winder.glb +0 -0
- package/public/assets/game/npc/boltnu-sheet.png +0 -0
- package/public/assets/game/npc/fluttaflap-sheet.png +0 -0
- package/public/assets/game/npc/rockitten-sheet.png +0 -0
- package/public/assets/game/npc/tux-sheet.png +0 -0
- package/public/assets/game/player/misa-back-walk.000.png +0 -0
- package/public/assets/game/player/misa-back-walk.001.png +0 -0
- package/public/assets/game/player/misa-back-walk.002.png +0 -0
- package/public/assets/game/player/misa-back-walk.003.png +0 -0
- package/public/assets/game/player/misa-back.png +0 -0
- package/public/assets/game/player/misa-front-walk.000.png +0 -0
- package/public/assets/game/player/misa-front-walk.001.png +0 -0
- package/public/assets/game/player/misa-front-walk.002.png +0 -0
- package/public/assets/game/player/misa-front-walk.003.png +0 -0
- package/public/assets/game/player/misa-front.png +0 -0
- package/public/assets/game/player/misa-left-walk.000.png +0 -0
- package/public/assets/game/player/misa-left-walk.001.png +0 -0
- package/public/assets/game/player/misa-left-walk.002.png +0 -0
- package/public/assets/game/player/misa-left-walk.003.png +0 -0
- package/public/assets/game/player/misa-left.png +0 -0
- package/public/assets/game/player/misa-right-walk.000.png +0 -0
- package/public/assets/game/player/misa-right-walk.001.png +0 -0
- package/public/assets/game/player/misa-right-walk.002.png +0 -0
- package/public/assets/game/player/misa-right-walk.003.png +0 -0
- package/public/assets/game/player/misa-right.png +0 -0
- package/public/assets/game/player-sheet.png +0 -0
- package/public/assets/game/tuxemon-town.json +1 -0
- package/public/assets/game/tuxmon-sample-32px-extruded.png +0 -0
- package/public/assets/game/wuwa/scene-map.png +0 -0
- package/public/assets/game/wuwa/world-far.png +0 -0
- package/public/assets/game/wuwa/world-middle.png +0 -0
- package/public/assets/game/wuwa/world-near.png +0 -0
- package/public/assets/game/wuwa/wu-aura.png +0 -0
- package/public/assets/game/wuwa/wu-lead.png +0 -0
- package/public/assets/game/wuwa/wu-ring.png +0 -0
- package/public/assets/game/wuwa/wu-slash.png +0 -0
- package/public/assets/game/wuwa/wu-worker-0.png +0 -0
- package/public/assets/game/wuwa/wu-worker-1.png +0 -0
- package/public/assets/game/wuwa/wu-worker-2.png +0 -0
- package/public/favicon.svg +14 -0
- package/public/scene/background/cyber-town-background.svg +110 -0
- package/scripts/_daq-realtime-probe.mjs +68 -0
- package/scripts/_daq-warn-probe.mjs +50 -0
- package/scripts/_dbg-agent-tools-audit.mjs +154 -0
- package/scripts/_dbg-agent-workflow-audit.mjs +177 -0
- package/scripts/_dbg-anim-verify.mjs +81 -0
- package/scripts/_dbg-audit-all-pages.mjs +43 -0
- package/scripts/_dbg-autoframe.mjs +103 -0
- package/scripts/_dbg-beacon-final.mjs +181 -0
- package/scripts/_dbg-bind-bidi-e2e.mjs +239 -0
- package/scripts/_dbg-callout-near.mjs +108 -0
- package/scripts/_dbg-chan-focus.mjs +49 -0
- package/scripts/_dbg-cleanup.mjs +31 -0
- package/scripts/_dbg-closedloop-ui-shot.mjs +61 -0
- package/scripts/_dbg-console-audit.mjs +61 -0
- package/scripts/_dbg-console-probe.mjs +23 -0
- package/scripts/_dbg-context-inject-audit.mjs +231 -0
- package/scripts/_dbg-crash-recovery.mjs +143 -0
- package/scripts/_dbg-crop-shot.mjs +16 -0
- package/scripts/_dbg-daq-acc-probe.mjs +24 -0
- package/scripts/_dbg-daq-attrib.mjs +34 -0
- package/scripts/_dbg-daq-bind-audit.mjs +119 -0
- package/scripts/_dbg-daq-cadence.mjs +104 -0
- package/scripts/_dbg-daq-debug.mjs +22 -0
- package/scripts/_dbg-daq-diag.mjs +33 -0
- package/scripts/_dbg-daq-e2e.mjs +171 -0
- package/scripts/_dbg-daq-film.mjs +136 -0
- package/scripts/_dbg-daq-filter-e2e.mjs +109 -0
- package/scripts/_dbg-daq-filter-e2e2.mjs +62 -0
- package/scripts/_dbg-daq-final-regression.mjs +78 -0
- package/scripts/_dbg-daq-full-check.mjs +179 -0
- package/scripts/_dbg-daq-line-note-verify.mjs +61 -0
- package/scripts/_dbg-daq-linebanner-audit.mjs +107 -0
- package/scripts/_dbg-daq-pages-shot.mjs +37 -0
- package/scripts/_dbg-daq-perf.mjs +61 -0
- package/scripts/_dbg-daq-probe.mjs +27 -0
- package/scripts/_dbg-daq-recipe-e2e.mjs +30 -0
- package/scripts/_dbg-daq-redesign-shot.mjs +88 -0
- package/scripts/_dbg-daq-rows-probe.mjs +26 -0
- package/scripts/_dbg-daq-rowstate-e2e.mjs +152 -0
- package/scripts/_dbg-daq-rowstate-shot.mjs +45 -0
- package/scripts/_dbg-daq-rowstate-shot2.mjs +38 -0
- package/scripts/_dbg-daq-tpl.mjs +144 -0
- package/scripts/_dbg-dash-console-check.mjs +49 -0
- package/scripts/_dbg-dash-deep-probe.mjs +61 -0
- package/scripts/_dbg-dash-hitpoint.mjs +44 -0
- package/scripts/_dbg-dash-live-shot.mjs +36 -0
- package/scripts/_dbg-dash-shot.mjs +21 -0
- package/scripts/_dbg-dashboard-shot.mjs +35 -0
- package/scripts/_dbg-dcw-audit.mjs +126 -0
- package/scripts/_dbg-dcw-card-e2e.mjs +108 -0
- package/scripts/_dbg-dcw-channel-ui.mjs +144 -0
- package/scripts/_dbg-dcw-examples.mjs +81 -0
- package/scripts/_dbg-dcw-light-shot.mjs +19 -0
- package/scripts/_dbg-dcw-line-audit.mjs +107 -0
- package/scripts/_dbg-dcw-pause-tpl-audit.mjs +161 -0
- package/scripts/_dbg-dcw-pause-ui.mjs +175 -0
- package/scripts/_dbg-dcw-scene.mjs +72 -0
- package/scripts/_dbg-dcw-shot2.mjs +14 -0
- package/scripts/_dbg-dcw-twin-e2e.mjs +192 -0
- package/scripts/_dbg-dt-interact.mjs +193 -0
- package/scripts/_dbg-error-probe.mjs +54 -0
- package/scripts/_dbg-filter-light-verify.mjs +54 -0
- package/scripts/_dbg-final-acceptance-2.mjs +72 -0
- package/scripts/_dbg-final-acceptance.mjs +192 -0
- package/scripts/_dbg-final-agent-e2e.mjs +151 -0
- package/scripts/_dbg-final-full-e2e.mjs +218 -0
- package/scripts/_dbg-final-line-e2e.mjs +173 -0
- package/scripts/_dbg-final-t6-hitl.mjs +38 -0
- package/scripts/_dbg-focus-range.mjs +115 -0
- package/scripts/_dbg-fresh-scene-e2e.mjs +125 -0
- package/scripts/_dbg-full-feature-e2e.mjs +219 -0
- package/scripts/_dbg-fullchain-e2e.mjs +178 -0
- package/scripts/_dbg-gate-debug.mjs +37 -0
- package/scripts/_dbg-gif-encode.mjs +111 -0
- package/scripts/_dbg-gizmo.mjs +95 -0
- package/scripts/_dbg-glass-probe.mjs +57 -0
- package/scripts/_dbg-glass-shot.mjs +50 -0
- package/scripts/_dbg-glass-sweep.mjs +25 -0
- package/scripts/_dbg-glb-probe.mjs +43 -0
- package/scripts/_dbg-hardening-probe.mjs +90 -0
- package/scripts/_dbg-hero-btn-probe.mjs +21 -0
- package/scripts/_dbg-hidden-check.mjs +21 -0
- package/scripts/_dbg-hitl-timeout-e2e.mjs +93 -0
- package/scripts/_dbg-hmi-audit.mjs +164 -0
- package/scripts/_dbg-home-repro.mjs +24 -0
- package/scripts/_dbg-home-stack.mjs +13 -0
- package/scripts/_dbg-huashu-survey.mjs +31 -0
- package/scripts/_dbg-huashu-survey2.mjs +35 -0
- package/scripts/_dbg-huashu-verify.mjs +25 -0
- package/scripts/_dbg-huashu-verify2.mjs +26 -0
- package/scripts/_dbg-hydration-detail.mjs +18 -0
- package/scripts/_dbg-i18n-debug.mjs +27 -0
- package/scripts/_dbg-i18n-debug2.mjs +26 -0
- package/scripts/_dbg-i18n-final.mjs +42 -0
- package/scripts/_dbg-i18n-scan.mjs +38 -0
- package/scripts/_dbg-infra-lifecycle.mjs +44 -0
- package/scripts/_dbg-line-aura-shots.mjs +54 -0
- package/scripts/_dbg-line-control-e2e.mjs +120 -0
- package/scripts/_dbg-line-gate-audit.mjs +108 -0
- package/scripts/_dbg-line-live-e2e.mjs +144 -0
- package/scripts/_dbg-line-scenario-e2e.mjs +256 -0
- package/scripts/_dbg-minimal-omp.mjs +77 -0
- package/scripts/_dbg-mode-nav.mjs +173 -0
- package/scripts/_dbg-mqtt-chain-e2e.mjs +90 -0
- package/scripts/_dbg-nav-rpg.mjs +84 -0
- package/scripts/_dbg-nav-shot.mjs +28 -0
- package/scripts/_dbg-nav.mjs +27 -0
- package/scripts/_dbg-opslog-e2e.mjs +121 -0
- package/scripts/_dbg-opslog-shots.mjs +62 -0
- package/scripts/_dbg-opt-team-e2e.mjs +133 -0
- package/scripts/_dbg-optimization-e2e.mjs +94 -0
- package/scripts/_dbg-orbit.mjs +58 -0
- package/scripts/_dbg-panesplitter-fix-check.mjs +62 -0
- package/scripts/_dbg-phase-e3-monitor.mjs +25 -0
- package/scripts/_dbg-pop-clamp-verify.mjs +77 -0
- package/scripts/_dbg-preset-e2e.mjs +116 -0
- package/scripts/_dbg-preview-console.mjs +28 -0
- package/scripts/_dbg-preview-minrepro.mjs +156 -0
- package/scripts/_dbg-preview-pixels.mjs +93 -0
- package/scripts/_dbg-quality-pages-e2e.mjs +84 -0
- package/scripts/_dbg-query-cancel.ts +12 -0
- package/scripts/_dbg-rail-tree-e2e.mjs +183 -0
- package/scripts/_dbg-real-driver-e2e.mjs +55 -0
- package/scripts/_dbg-redesign-shots.mjs +34 -0
- package/scripts/_dbg-restart-persist.mjs +41 -0
- package/scripts/_dbg-rollback-mgr-audit.mjs +169 -0
- package/scripts/_dbg-rpg.mjs +71 -0
- package/scripts/_dbg-sc001-dispatch.mjs +63 -0
- package/scripts/_dbg-scenario-final-shot.mjs +24 -0
- package/scripts/_dbg-scenario-phase-e2.mjs +75 -0
- package/scripts/_dbg-scenario-phase-e3.mjs +96 -0
- package/scripts/_dbg-scenario-phase1.mjs +90 -0
- package/scripts/_dbg-scenario-phase2.mjs +105 -0
- package/scripts/_dbg-scenario-phase3-monitor.mjs +48 -0
- package/scripts/_dbg-scenario-phase3.mjs +111 -0
- package/scripts/_dbg-scene-polish-shot.mjs +118 -0
- package/scripts/_dbg-selring-probe.mjs +47 -0
- package/scripts/_dbg-sideinfo.mjs +101 -0
- package/scripts/_dbg-splitter.mjs +88 -0
- package/scripts/_dbg-team-repro.mjs +76 -0
- package/scripts/_dbg-town-alarm-e2e.mjs +40 -0
- package/scripts/_dbg-town-beauty-shots.mjs +54 -0
- package/scripts/_dbg-town-daq-shot.mjs +26 -0
- package/scripts/_dbg-town-full-e2e.mjs +104 -0
- package/scripts/_dbg-town-gate.mjs +44 -0
- package/scripts/_dbg-town-ins-probe.mjs +37 -0
- package/scripts/_dbg-town-perf.mjs +50 -0
- package/scripts/_dbg-town-polish-shots.mjs +82 -0
- package/scripts/_dbg-town-realtime.mjs +42 -0
- package/scripts/_dbg-town-shot3.mjs +17 -0
- package/scripts/_dbg-town-truth.mjs +37 -0
- package/scripts/_dbg-town-truth2.mjs +27 -0
- package/scripts/_dbg-transform-audit.mjs +98 -0
- package/scripts/_dbg-trend-expand-test.mjs +38 -0
- package/scripts/_dbg-twindaq-focus.mjs +81 -0
- package/scripts/_dbg-ui-polish-shot.mjs +43 -0
- package/scripts/_dbg-ui-polish-verify1.mjs +92 -0
- package/scripts/_dbg-ui-polish-verify2.mjs +50 -0
- package/scripts/_dbg-ui-polish-verify3.mjs +51 -0
- package/scripts/_dbg-ui-polish-verify4.mjs +50 -0
- package/scripts/_dbg-ui-shot.mjs +26 -0
- package/scripts/_dbg-unify-shot.mjs +43 -0
- package/scripts/_dbg-uplink-probe.mjs +36 -0
- package/scripts/_dbg-wizard-preview-shot.mjs +58 -0
- package/scripts/_dbg-wizard-ui-e2e.mjs +93 -0
- package/scripts/_dbg-ws-probe.mjs +86 -0
- package/scripts/_dbg-ws-shot2.mjs +23 -0
- package/scripts/_dbg-wsdot-sanity.mjs +25 -0
- package/scripts/_dev-stack.mjs +46 -0
- package/scripts/_http-driver-probe.mjs +30 -0
- package/scripts/_lib-dcw-line.mjs +24 -0
- package/scripts/_mqtt-driver-probe.mjs +30 -0
- package/scripts/_newdriver-rest-probe.mjs +100 -0
- package/scripts/_node-state-probe.mjs +5 -0
- package/scripts/_opcua-driver-probe.mjs +31 -0
- package/scripts/_rtu-debug.mjs +41 -0
- package/scripts/_rtu-direct.mjs +6 -0
- package/scripts/_rtu-driver-probe.mjs +53 -0
- package/scripts/_rtu-mini-slave.mjs +63 -0
- package/scripts/_townview_style.css +1185 -0
- package/scripts/_townview_template.html +1215 -0
- package/scripts/_wizard-shots.mjs +90 -0
- package/scripts/acceptance-suite.py +207 -0
- package/scripts/api-live-e2e.mjs +467 -0
- package/scripts/build-anime-models.mjs +339 -0
- package/scripts/build-character-models.mjs +175 -0
- package/scripts/build-film-devices.mjs +227 -0
- package/scripts/build-model-3d.mjs +173 -0
- package/scripts/build-player-sheet.mjs +58 -0
- package/scripts/build-world-map.mjs +188 -0
- package/scripts/build-wuwa-assets.mjs +345 -0
- package/scripts/collab-live-test.py +177 -0
- package/scripts/debug-mcp-http.ts +64 -0
- package/scripts/demo-lead-worker.ts +256 -0
- package/scripts/demo-modes.ts +415 -0
- package/scripts/demo-omp-channel.ts +135 -0
- package/scripts/design-preflight.mjs +223 -0
- package/scripts/design-verify-templates.mjs +110 -0
- package/scripts/design-verify.mjs +170 -0
- package/scripts/dev-guard.mjs +74 -0
- package/scripts/dev-modbus-simulator.mjs +55 -0
- package/scripts/dev-opcua-simulator.mjs +57 -0
- package/scripts/dev-protocol-simulators.mjs +145 -0
- package/scripts/dev-window.cmd +6 -0
- package/scripts/device-line-restore.mjs +34 -0
- package/scripts/diag-lead-status.ts +101 -0
- package/scripts/e2e-a2a-steer-live.mjs +193 -0
- package/scripts/e2e-agent-channel-task.ts +151 -0
- package/scripts/e2e-agent-team.mjs +168 -0
- package/scripts/e2e-auth-matrix.mjs +119 -0
- package/scripts/e2e-channel-isolation.ts +325 -0
- package/scripts/e2e-dual-drive-comm.ts +259 -0
- package/scripts/e2e-lead-team-mgmt.ts +217 -0
- package/scripts/e2e-memory-persistence.ts +320 -0
- package/scripts/e2e-memory-system.ts +332 -0
- package/scripts/e2e-omp-modes.ts +274 -0
- package/scripts/e2e-omp-realtime.ts +258 -0
- package/scripts/e2e-omp-workspace.ts +180 -0
- package/scripts/e2e-parallel-execution.mjs +151 -0
- package/scripts/e2e-rest-memory.mjs +251 -0
- package/scripts/e2e-rest-robustness.mjs +177 -0
- package/scripts/e2e-resume-crash.mjs +254 -0
- package/scripts/e2e-tab-close.mjs +136 -0
- package/scripts/e2e-task-queue.ts +277 -0
- package/scripts/e2e-template-isolation.mjs +267 -0
- package/scripts/e2e-token-ui.mjs +149 -0
- package/scripts/final-soup-run.py +192 -0
- package/scripts/final-soup-watch.py +83 -0
- package/scripts/home-bootstrap.mjs +117 -0
- package/scripts/i18n-codemod.mjs +173 -0
- package/scripts/i18n-fix-en.py +28 -0
- package/scripts/i18n-fix-en2.py +31 -0
- package/scripts/i18n-merge.mjs +67 -0
- package/scripts/omp-real-test.py +328 -0
- package/scripts/smoke-optimization-verify.ts +72 -0
- package/scripts/start.mjs +60 -0
- package/scripts/test-a2a-relay.mjs +222 -0
- package/scripts/test-a2a-types.ts +296 -0
- package/scripts/test-agent-brain.ts +135 -0
- package/scripts/test-agent-multi-channel.ts +160 -0
- package/scripts/test-agent-runtime.ts +348 -0
- package/scripts/test-api-client.ts +108 -0
- package/scripts/test-api-suite.mjs +271 -0
- package/scripts/test-block-consumption.mjs +194 -0
- package/scripts/test-channel-routing.ts +301 -0
- package/scripts/test-cmd-render.mjs +218 -0
- package/scripts/test-collab-e2e.mjs +136 -0
- package/scripts/test-collab-omp.mjs +192 -0
- package/scripts/test-dedup-logic.ts +132 -0
- package/scripts/test-dual-drive.ts +227 -0
- package/scripts/test-e2e-collaboration.ts +262 -0
- package/scripts/test-e2e-monitor.ts +192 -0
- package/scripts/test-event-history.ts +112 -0
- package/scripts/test-event-persistence.mjs +169 -0
- package/scripts/test-exec-modes.ts +316 -0
- package/scripts/test-fifo-realtime.mjs +193 -0
- package/scripts/test-final-e2e.mjs +290 -0
- package/scripts/test-full-system.ts +439 -0
- package/scripts/test-game-session.ts +160 -0
- package/scripts/test-goal-summary.ts +202 -0
- package/scripts/test-harness-liveness.ts +109 -0
- package/scripts/test-harness-terminal.ts +164 -0
- package/scripts/test-header-trail.mjs +95 -0
- package/scripts/test-hitl.ts +360 -0
- package/scripts/test-koda-restyle.mjs +105 -0
- package/scripts/test-lane-history-live.mjs +103 -0
- package/scripts/test-lane-raw.ts +67 -0
- package/scripts/test-lanes-history-render.mjs +138 -0
- package/scripts/test-loop-omp.mjs +154 -0
- package/scripts/test-mcp-tools.ts +285 -0
- package/scripts/test-mdlite.ts +53 -0
- package/scripts/test-memory-dynamic.ts +99 -0
- package/scripts/test-memory-maintenance.ts +117 -0
- package/scripts/test-memory-vector.ts +154 -0
- package/scripts/test-memory.ts +431 -0
- package/scripts/test-mock-collab.mjs +226 -0
- package/scripts/test-modern-render.mjs +174 -0
- package/scripts/test-omp-terminal.mjs +187 -0
- package/scripts/test-orchestration.ts +425 -0
- package/scripts/test-persistence-lazy.ts +323 -0
- package/scripts/test-prompt-system.mjs +181 -0
- package/scripts/test-prompts-loader.ts +56 -0
- package/scripts/test-protocol.ts +156 -0
- package/scripts/test-real-scenario.mjs +290 -0
- package/scripts/test-route-reason.mjs +111 -0
- package/scripts/test-scheduler-loop.ts +469 -0
- package/scripts/test-stream-render.ts +37 -0
- package/scripts/test-task-drawer.mjs +204 -0
- package/scripts/test-task-engine.ts +433 -0
- package/scripts/test-task-invariants.mjs +169 -0
- package/scripts/test-terminal-e2e.mjs +264 -0
- package/scripts/test-town-3d.mjs +121 -0
- package/scripts/test-town-drag.mjs +218 -0
- package/scripts/test-town-layout.mjs +200 -0
- package/scripts/test-town-page.mjs +136 -0
- package/scripts/test-town-scene-math.ts +169 -0
- package/scripts/test-town-twin.mjs +121 -0
- package/scripts/test-town-upload.mjs +89 -0
- package/scripts/test-town-user-flow.mjs +619 -0
- package/scripts/test-town-world.mjs +76 -0
- package/scripts/test-town-wuwa.mjs +198 -0
- package/scripts/test-user-isolation.mjs +193 -0
- package/scripts/test-workshop-db.ts +280 -0
- package/scripts/test-workshop-entries.ts +370 -0
- package/scripts/test-workspace-api.ts +329 -0
- package/scripts/test-ws-aep.mjs +187 -0
- package/scripts/test-ws-protocol.ts +134 -0
- package/scripts/test-ws-stress.mjs +208 -0
- package/scripts/town-demo-seed.mjs +75 -0
- package/scripts/town-scene-clear.mjs +56 -0
- package/scripts/ui-screenshot.mjs +128 -0
- package/scripts/verify-a2a-live.mjs +124 -0
- package/scripts/verify-exec-modes-report.ts +248 -0
- package/scripts/verify-game.mjs +477 -0
- package/scripts/verify-lead-mail.ts +188 -0
- package/scripts/verify-page-smoke.mjs +87 -0
- package/scripts/verify-ws-events.mjs +123 -0
- package/scripts/ws-test.mjs +176 -0
- package/scripts/ws-test.py +4 -0
- package/server/api/health.get.ts +32 -0
- package/server/api/mcp/workshop.ts +78 -0
- package/server/api/metrics.ts +75 -0
- package/server/api/system/config.get.ts +12 -0
- package/server/api/system/monitor/terminal/ws.ts +177 -0
- package/server/api/system/monitor/terminate.post.ts +36 -0
- package/server/api/system/monitor.get.ts +13 -0
- package/server/api/system/settings/events.get.ts +28 -0
- package/server/api/system/settings/index.get.ts +14 -0
- package/server/api/system/settings/index.patch.ts +22 -0
- package/server/api/system/settings/reset.post.ts +12 -0
- package/server/api/users/[id].delete.ts +11 -0
- package/server/api/users/[id].get.ts +11 -0
- package/server/api/users/[id].put.ts +14 -0
- package/server/api/users/index.get.ts +13 -0
- package/server/api/users/index.post.ts +13 -0
- package/server/api/users/login.post.ts +11 -0
- package/server/api/users/logout.post.ts +9 -0
- package/server/api/users/me.get.ts +9 -0
- package/server/api/users/register.post.ts +11 -0
- package/server/api/users/tokens/[id]/token.get.ts +11 -0
- package/server/api/users/tokens/[id].delete.ts +11 -0
- package/server/api/users/tokens/[id].patch.ts +14 -0
- package/server/api/users/tokens/index.get.ts +9 -0
- package/server/api/users/tokens/index.post.ts +13 -0
- package/server/api/workshop/[...path].ts +13 -0
- package/server/api/workshop/a2a/[agentId]/card/index.get.ts +75 -0
- package/server/api/workshop/a2a/[agentId]/rpc/index.post.ts +429 -0
- package/server/api/workshop/a2a/send.post.ts +28 -0
- package/server/api/workshop/agent-tools/approvals/[id]/decide.post.ts +21 -0
- package/server/api/workshop/agent-tools/approvals/index.get.ts +18 -0
- package/server/api/workshop/agent-tools/bindings/[id].delete.ts +21 -0
- package/server/api/workshop/agent-tools/bindings/[id].patch.ts +14 -0
- package/server/api/workshop/agent-tools/bindings/index.get.ts +14 -0
- package/server/api/workshop/agent-tools/bindings/index.post.ts +20 -0
- package/server/api/workshop/agent-tools/invoke.post.ts +34 -0
- package/server/api/workshop/agents/[id].delete.ts +20 -0
- package/server/api/workshop/agents/[id].get.ts +19 -0
- package/server/api/workshop/agents/[id].patch.ts +30 -0
- package/server/api/workshop/agents/index.get.ts +13 -0
- package/server/api/workshop/agents/index.post.ts +29 -0
- package/server/api/workshop/agents/subscribe.post.ts +21 -0
- package/server/api/workshop/approvals/[id]/decide.post.ts +30 -0
- package/server/api/workshop/approvals/index.get.ts +22 -0
- package/server/api/workshop/assets/character/[id].delete.ts +33 -0
- package/server/api/workshop/assets/character/index.get.ts +17 -0
- package/server/api/workshop/assets/character/index.post.ts +87 -0
- package/server/api/workshop/assets/devices/[id].delete.ts +43 -0
- package/server/api/workshop/assets/devices/index.get.ts +103 -0
- package/server/api/workshop/assets/devices/index.post.ts +53 -0
- package/server/api/workshop/audit.get.ts +43 -0
- package/server/api/workshop/caller.ts +141 -0
- package/server/api/workshop/channel-templates/[id]/instantiate.post.ts +31 -0
- package/server/api/workshop/channel-templates/[id].delete.ts +20 -0
- package/server/api/workshop/channel-templates/[id].get.ts +19 -0
- package/server/api/workshop/channel-templates/[id].patch.ts +48 -0
- package/server/api/workshop/channel-templates/from-channel.post.ts +31 -0
- package/server/api/workshop/channel-templates/index.get.ts +13 -0
- package/server/api/workshop/channel-templates/index.post.ts +61 -0
- package/server/api/workshop/channels/[id]/activate.post.ts +19 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/memories/[memoryId].delete.ts +30 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/memories/index.get.ts +29 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/memories/index.post.ts +48 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/memories/search.post.ts +43 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/model.patch.ts +39 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/position.patch.ts +33 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/range.patch.ts +54 -0
- package/server/api/workshop/channels/[id]/agents/[agentId]/stop.post.ts +25 -0
- package/server/api/workshop/channels/[id]/agents/[agentId].delete.ts +23 -0
- package/server/api/workshop/channels/[id]/agents/[agentId].patch.ts +28 -0
- package/server/api/workshop/channels/[id]/agents/index.get.ts +27 -0
- package/server/api/workshop/channels/[id]/agents/index.post.ts +61 -0
- package/server/api/workshop/channels/[id]/events.get.ts +55 -0
- package/server/api/workshop/channels/[id]/index.get.ts +18 -0
- package/server/api/workshop/channels/[id]/index.patch.ts +30 -0
- package/server/api/workshop/channels/[id]/memories/[memoryId].delete.ts +29 -0
- package/server/api/workshop/channels/[id]/memories/index.get.ts +28 -0
- package/server/api/workshop/channels/[id]/memories/index.post.ts +42 -0
- package/server/api/workshop/channels/[id]/messages/index.get.ts +56 -0
- package/server/api/workshop/channels/[id]/messages/index.post.ts +75 -0
- package/server/api/workshop/channels/[id]/queue.get.ts +25 -0
- package/server/api/workshop/channels/[id]/tasks/index.get.ts +24 -0
- package/server/api/workshop/channels/[id]/tasks/index.post.ts +79 -0
- package/server/api/workshop/channels/[id]/terminals.get.ts +21 -0
- package/server/api/workshop/channels/[id].delete.ts +18 -0
- package/server/api/workshop/channels.get.ts +11 -0
- package/server/api/workshop/channels.post.ts +48 -0
- package/server/api/workshop/daq/[id]/bind.post.ts +19 -0
- package/server/api/workshop/daq/[id]/samples.get.ts +22 -0
- package/server/api/workshop/daq/[id]/test.post.ts +17 -0
- package/server/api/workshop/daq/[id].delete.ts +30 -0
- package/server/api/workshop/daq/[id].patch.ts +31 -0
- package/server/api/workshop/daq/alarms/[id]/ack.post.ts +15 -0
- package/server/api/workshop/daq/alarms.get.ts +16 -0
- package/server/api/workshop/daq/controller.post.ts +52 -0
- package/server/api/workshop/daq/index.get.ts +40 -0
- package/server/api/workshop/daq/index.post.ts +33 -0
- package/server/api/workshop/daq/infra/reconnect.post.ts +17 -0
- package/server/api/workshop/daq/infra.get.ts +11 -0
- package/server/api/workshop/daq/templates/[key].delete.ts +17 -0
- package/server/api/workshop/daq/templates/[key].patch.ts +18 -0
- package/server/api/workshop/daq/templates/index.post.ts +17 -0
- package/server/api/workshop/daq/test-driver.post.ts +23 -0
- package/server/api/workshop/dcw/[id]/bind.post.ts +17 -0
- package/server/api/workshop/dcw/[id]/param-ledger.get.ts +15 -0
- package/server/api/workshop/dcw/[id]/test.post.ts +15 -0
- package/server/api/workshop/dcw/[id]/write.post.ts +19 -0
- package/server/api/workshop/dcw/[id].delete.ts +30 -0
- package/server/api/workshop/dcw/[id].patch.ts +18 -0
- package/server/api/workshop/dcw/controller.post.ts +43 -0
- package/server/api/workshop/dcw/index.get.ts +25 -0
- package/server/api/workshop/dcw/index.post.ts +20 -0
- package/server/api/workshop/dcw/journal/index.get.ts +21 -0
- package/server/api/workshop/dcw/journal/node/[nodeId]/rollback.post.ts +22 -0
- package/server/api/workshop/dcw/line/query.get.ts +31 -0
- package/server/api/workshop/dcw/lines/[id]/start.post.ts +35 -0
- package/server/api/workshop/dcw/lines/[id]/stop.post.ts +34 -0
- package/server/api/workshop/dcw/lines/[id].delete.ts +35 -0
- package/server/api/workshop/dcw/lines/[id].patch.ts +15 -0
- package/server/api/workshop/dcw/lines/index.get.ts +14 -0
- package/server/api/workshop/dcw/lines/index.post.ts +31 -0
- package/server/api/workshop/dcw/optimizations/[id]/judge.post.ts +24 -0
- package/server/api/workshop/dcw/optimizations/[id]/rollback.post.ts +29 -0
- package/server/api/workshop/dcw/optimizations/[id]/series.get.ts +18 -0
- package/server/api/workshop/dcw/optimizations/[id].get.ts +17 -0
- package/server/api/workshop/dcw/optimizations/index.get.ts +23 -0
- package/server/api/workshop/dcw/products/[id].delete.ts +16 -0
- package/server/api/workshop/dcw/products/[id].patch.ts +17 -0
- package/server/api/workshop/dcw/products/index.post.ts +16 -0
- package/server/api/workshop/dcw/recipes/[id]/apply.post.ts +40 -0
- package/server/api/workshop/dcw/recipes/[id]/mark-good.post.ts +21 -0
- package/server/api/workshop/dcw/recipes/[id]/rollback-good.post.ts +19 -0
- package/server/api/workshop/dcw/recipes/[id].delete.ts +14 -0
- package/server/api/workshop/dcw/recipes/[id].patch.ts +16 -0
- package/server/api/workshop/dcw/recipes/index.get.ts +12 -0
- package/server/api/workshop/dcw/recipes/index.post.ts +15 -0
- package/server/api/workshop/dcw/runs/[id]/close.post.ts +15 -0
- package/server/api/workshop/dcw/runs/[id]/data.get.ts +15 -0
- package/server/api/workshop/dcw/runs/[id]/rollback.post.ts +20 -0
- package/server/api/workshop/dcw/runs/index.get.ts +11 -0
- package/server/api/workshop/dcw/templates/[key].delete.ts +14 -0
- package/server/api/workshop/dcw/templates/[key].patch.ts +16 -0
- package/server/api/workshop/dcw/templates/index.post.ts +15 -0
- package/server/api/workshop/dcw/test-driver.post.ts +16 -0
- package/server/api/workshop/device-twins/[id]/control.post.ts +21 -0
- package/server/api/workshop/device-twins/[id]/telemetry.post.ts +21 -0
- package/server/api/workshop/device-twins/[id].delete.ts +25 -0
- package/server/api/workshop/device-twins/[id].patch.ts +29 -0
- package/server/api/workshop/device-twins/index.get.ts +16 -0
- package/server/api/workshop/device-twins/index.post.ts +32 -0
- package/server/api/workshop/fs/dirs.get.ts +40 -0
- package/server/api/workshop/mailbox/all.get.ts +19 -0
- package/server/api/workshop/mailbox.get.ts +14 -0
- package/server/api/workshop/memories/maintenance.post.ts +15 -0
- package/server/api/workshop/ops-logs/index.get.ts +30 -0
- package/server/api/workshop/ops-logs/index.post.ts +39 -0
- package/server/api/workshop/runtime.get.ts +11 -0
- package/server/api/workshop/scene/layouts/[channelId].delete.ts +23 -0
- package/server/api/workshop/scene/layouts/[channelId].put.ts +31 -0
- package/server/api/workshop/scene/layouts.get.ts +13 -0
- package/server/api/workshop/tasks/[id]/cancel/index.post.ts +34 -0
- package/server/api/workshop/tasks/[id]/complete.post.ts +32 -0
- package/server/api/workshop/tasks/[id]/dispatch.post.ts +30 -0
- package/server/api/workshop/tasks/[id]/report.post.ts +38 -0
- package/server/api/workshop/tasks/[id]/retry.post.ts +33 -0
- package/server/api/workshop/tasks/[id].get.ts +32 -0
- package/server/api/workshop/tasks.get.ts +13 -0
- package/server/api/workshop/teams/[id]/deploy.post.ts +40 -0
- package/server/api/workshop/teams/[id]/members/[agentId].delete.ts +19 -0
- package/server/api/workshop/teams/[id]/members/index.get.ts +19 -0
- package/server/api/workshop/teams/[id]/members/index.post.ts +32 -0
- package/server/api/workshop/teams/[id].delete.ts +20 -0
- package/server/api/workshop/teams/[id].get.ts +19 -0
- package/server/api/workshop/teams/[id].patch.ts +32 -0
- package/server/api/workshop/teams/index.get.ts +13 -0
- package/server/api/workshop/teams/index.post.ts +27 -0
- package/server/api/workshop/users/me.get.ts +23 -0
- package/server/api/workshop/users/register.post.ts +33 -0
- package/server/api/workshop/workspaces/[id]/channel-templates/[tplId].post.ts +31 -0
- package/server/api/workshop/workspaces/[id]/channels/[channelId].delete.ts +15 -0
- package/server/api/workshop/workspaces/[id]/channels/[channelId].post.ts +15 -0
- package/server/api/workshop/workspaces/[id]/index.delete.ts +14 -0
- package/server/api/workshop/workspaces/index.get.ts +11 -0
- package/server/api/workshop/workspaces/index.post.ts +17 -0
- package/server/api/workshop/ws.ts +694 -0
- package/server/data/agent-node-bindings.json +554 -0
- package/server/data/daq-templates.json +16 -0
- package/server/data/daqs.json +2680 -0
- package/server/data/dcw-lines.json +919 -0
- package/server/data/dcw-products.json +835 -0
- package/server/data/dcw-recipes.json +1650 -0
- package/server/data/dcw-rollback.json +9339 -0
- package/server/data/dcw-runs.json +4633 -0
- package/server/data/dcw-templates.json +26 -0
- package/server/data/dcw-writes.json +9770 -0
- package/server/data/dcws.json +1736 -0
- package/server/data/device-twins.json +133 -0
- package/server/data/line-runs.json +152 -0
- package/server/data/scene-layouts.json +200 -0
- package/server/mcp/workshop-server.ts +510 -0
- package/server/middleware/request-log.ts +15 -0
- package/server/plugins/backup.ts +85 -0
- package/server/plugins/daq.ts +65 -0
- package/server/plugins/dev-stability-guard.ts +42 -0
- package/server/plugins/security.ts +29 -0
- package/server/plugins/system-config.ts +38 -0
- package/server/plugins/workshop.ts +145 -0
- package/server/repositories/user.repository.ts +391 -0
- package/server/schemas/user.schema.ts +67 -0
- package/server/services/system-config.ts +356 -0
- package/server/services/user.service.ts +186 -0
- package/server/services/workshop/agents/adapters/omp-rpc-client.ts +539 -0
- package/server/services/workshop/agents/adapters/to-a2a.ts +40 -0
- package/server/services/workshop/agents/agent-interface.ts +219 -0
- package/server/services/workshop/agents/claude-agent.ts +47 -0
- package/server/services/workshop/agents/factory.ts +24 -0
- package/server/services/workshop/agents/harness-process.ts +158 -0
- package/server/services/workshop/agents/harness-terminal.ts +545 -0
- package/server/services/workshop/agents/industrial-context.ts +222 -0
- package/server/services/workshop/agents/industrial-tools.ts +362 -0
- package/server/services/workshop/agents/mock-agent.ts +396 -0
- package/server/services/workshop/agents/node-bindings.repo.ts +165 -0
- package/server/services/workshop/agents/omp-agent.ts +1598 -0
- package/server/services/workshop/agents/tool-approvals.ts +174 -0
- package/server/services/workshop/assets/character-asset.repo.ts +120 -0
- package/server/services/workshop/assets/device-twin.repo.ts +236 -0
- package/server/services/workshop/daq/alarm-notify.ts +100 -0
- package/server/services/workshop/daq/bus/index.ts +46 -0
- package/server/services/workshop/daq/bus/inproc.adapter.ts +62 -0
- package/server/services/workshop/daq/bus/mqtt.adapter.ts +112 -0
- package/server/services/workshop/daq/bus/queue-port.ts +36 -0
- package/server/services/workshop/daq/daq-controller.ts +778 -0
- package/server/services/workshop/daq/daq-node.repo.ts +96 -0
- package/server/services/workshop/daq/daq-node.ts +247 -0
- package/server/services/workshop/daq/daq-runtime.ts +168 -0
- package/server/services/workshop/daq/daq-templates.ts +157 -0
- package/server/services/workshop/daq/drivers.ts +771 -0
- package/server/services/workshop/daq/host-bindings.ts +71 -0
- package/server/services/workshop/daq/host-ports.ts +64 -0
- package/server/services/workshop/daq/infra.ts +202 -0
- package/server/services/workshop/daq/storage/index.ts +96 -0
- package/server/services/workshop/daq/storage/sqlite.adapter.ts +205 -0
- package/server/services/workshop/daq/storage/timescale.adapter.ts +185 -0
- package/server/services/workshop/daq/storage/tsdb-port.ts +74 -0
- package/server/services/workshop/db/agent.repo.ts +105 -0
- package/server/services/workshop/db/channel-agent.repo.ts +119 -0
- package/server/services/workshop/db/channel-event.repo.ts +154 -0
- package/server/services/workshop/db/channel-template.repo.ts +113 -0
- package/server/services/workshop/db/channel.repo.ts +101 -0
- package/server/services/workshop/db/database.ts +934 -0
- package/server/services/workshop/db/memory.repo.ts +249 -0
- package/server/services/workshop/db/message.repo.ts +149 -0
- package/server/services/workshop/db/ops.repo.ts +323 -0
- package/server/services/workshop/db/schema.sql +143 -0
- package/server/services/workshop/db/subscription.repo.ts +62 -0
- package/server/services/workshop/db/task.repo.ts +175 -0
- package/server/services/workshop/db/team-member.repo.ts +70 -0
- package/server/services/workshop/db/team.repo.ts +109 -0
- package/server/services/workshop/db/user.repo.ts +100 -0
- package/server/services/workshop/dcw/dcw-controller.ts +841 -0
- package/server/services/workshop/dcw/dcw-line.repo.ts +86 -0
- package/server/services/workshop/dcw/dcw-node.repo.ts +89 -0
- package/server/services/workshop/dcw/dcw-node.ts +200 -0
- package/server/services/workshop/dcw/dcw-product.repo.ts +95 -0
- package/server/services/workshop/dcw/dcw-recipe.repo.ts +349 -0
- package/server/services/workshop/dcw/dcw-runtime.ts +82 -0
- package/server/services/workshop/dcw/dcw-templates.ts +138 -0
- package/server/services/workshop/dcw/drivers.ts +523 -0
- package/server/services/workshop/dcw/line-run.ts +101 -0
- package/server/services/workshop/dcw/recipe-rollback-manager.ts +702 -0
- package/server/services/workshop/dcw/recipe-rollback.repo.ts +197 -0
- package/server/services/workshop/logger.ts +58 -0
- package/server/services/workshop/ops/ops.ts +78 -0
- package/server/services/workshop/prompts/loader.ts +112 -0
- package/server/services/workshop/runtime/agent-runtime.ts +698 -0
- package/server/services/workshop/runtime/channel-runtime.ts +202 -0
- package/server/services/workshop/runtime/embedding-provider.ts +79 -0
- package/server/services/workshop/runtime/execution-mode.ts +215 -0
- package/server/services/workshop/runtime/mailbox.ts +148 -0
- package/server/services/workshop/runtime/manager.ts +2543 -0
- package/server/services/workshop/runtime/memory.ts +439 -0
- package/server/services/workshop/runtime/monitor.ts +194 -0
- package/server/services/workshop/runtime/scheduler-loop.ts +773 -0
- package/server/services/workshop/runtime/task-engine.ts +574 -0
- package/server/services/workshop/scene/scene-layout.repo.ts +112 -0
- package/server/services/workshop/scene-events.ts +63 -0
- package/server/services/workshop/types/a2a.ts +61 -0
- package/server/services/workshop/types/task.ts +85 -0
- package/server/types/user.ts +53 -0
- package/server/utils/approval-gate.ts +67 -0
- package/server/utils/auth.ts +13 -0
- package/server/utils/config.ts +40 -0
- package/server/utils/errors.ts +24 -0
- package/server/utils/response.ts +49 -0
- package/server/utils/validate.ts +19 -0
- package/shared/config/engine.d.mts +41 -0
- package/shared/config/engine.mjs +291 -0
- package/shared/config/home.d.mts +48 -0
- package/shared/config/home.mjs +146 -0
- package/shared/config/schema.json +30 -0
- package/shared/daq-protocol.ts +361 -0
- package/shared/dcw-protocol.ts +576 -0
- package/shared/terminal-protocol.ts +70 -0
- package/shared/town-anim.ts +56 -0
- package/shared/town-behavior.ts +136 -0
- package/shared/town-protocol.ts +110 -0
- package/shared/town-scene-math.ts +283 -0
- package/shared/workshop-protocol.ts +199 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|