crewly 1.20.133 → 1.20.135
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/config/skills/agent/core/block-task/SKILL.md +16 -3
- package/dist/backend/backend/src/constants.d.ts +82 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +87 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.d.ts +44 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.js +122 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.d.ts +25 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.js +33 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts +6 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +9 -2
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +28 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +4 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +43 -7
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +144 -51
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/registration-flow-registry.d.ts +92 -0
- package/dist/backend/backend/src/services/agent/registration-flow-registry.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/registration-flow-registry.js +121 -0
- package/dist/backend/backend/src/services/agent/registration-flow-registry.js.map +1 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.d.ts +66 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.js +72 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.d.ts +62 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.js +173 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.d.ts +359 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.js +888 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.d.ts +167 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.js +248 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.d.ts +43 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.js +57 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.d.ts +37 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.js +60 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.d.ts +93 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.js +532 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.d.ts +100 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.js +183 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.d.ts +36 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.js +38 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.d.ts +105 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.js +144 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.d.ts +48 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.js +90 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.d.ts +81 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.js +172 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.d.ts +59 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.js +98 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +5 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/core/storage.service.js +1 -1
- package/dist/backend/backend/src/services/core/storage.service.js.map +1 -1
- package/dist/backend/backend/src/services/core/teams-backup.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/core/teams-backup.service.js +4 -2
- package/dist/backend/backend/src/services/core/teams-backup.service.js.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.d.ts.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.js +2 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.js.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.d.ts +14 -2
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.js +37 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +9 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +8 -0
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +21 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +85 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +32 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +60 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +5 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +4 -2
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/types/solution-bundle.types.d.ts +367 -0
- package/dist/backend/backend/src/types/solution-bundle.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/solution-bundle.types.js +17 -0
- package/dist/backend/backend/src/types/solution-bundle.types.js.map +1 -0
- package/dist/backend/backend/src/types/team-template.types.d.ts +8 -0
- package/dist/backend/backend/src/types/team-template.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/team-template.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts +37 -0
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.js +33 -0
- package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +82 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +87 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +43 -7
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +144 -51
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/registration-flow-registry.d.ts +92 -0
- package/dist/cli/backend/src/services/agent/registration-flow-registry.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/registration-flow-registry.js +121 -0
- package/dist/cli/backend/src/services/agent/registration-flow-registry.js.map +1 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.d.ts +66 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.js +72 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.d.ts +359 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.js +888 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.d.ts +167 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.js +248 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.d.ts +37 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.js +60 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.d.ts +93 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.js +532 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.d.ts +100 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.js +183 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.d.ts +36 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.js +38 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.d.ts +105 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.js +144 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.d.ts +48 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.js +90 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.d.ts +81 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.js +172 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.d.ts +59 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.js +98 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.js.map +1 -0
- package/dist/cli/backend/src/services/core/storage.service.js +1 -1
- package/dist/cli/backend/src/services/core/storage.service.js.map +1 -1
- package/dist/cli/backend/src/services/core/teams-backup.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/core/teams-backup.service.js +4 -2
- package/dist/cli/backend/src/services/core/teams-backup.service.js.map +1 -1
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.d.ts +72 -0
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.js +389 -0
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/index.d.ts +15 -0
- package/dist/cli/backend/src/services/messaging/index.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/index.js +20 -0
- package/dist/cli/backend/src/services/messaging/index.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts +224 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.js +969 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.d.ts +110 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.js +268 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.js.map +1 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.d.ts +14 -2
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.js +37 -1
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.js.map +1 -1
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts +133 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js +232 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.d.ts +277 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.js +708 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +21 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +85 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +32 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +60 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/template/template.service.js +5 -2
- package/dist/cli/backend/src/services/template/template.service.js.map +1 -1
- package/dist/cli/backend/src/services/workflow/cron-task.service.d.ts +316 -0
- package/dist/cli/backend/src/services/workflow/cron-task.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/workflow/cron-task.service.js +1043 -0
- package/dist/cli/backend/src/services/workflow/cron-task.service.js.map +1 -0
- package/dist/cli/backend/src/types/cron-task.types.d.ts +95 -0
- package/dist/cli/backend/src/types/cron-task.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/cron-task.types.js +10 -0
- package/dist/cli/backend/src/types/cron-task.types.js.map +1 -0
- package/dist/cli/backend/src/types/solution-bundle.types.d.ts +367 -0
- package/dist/cli/backend/src/types/solution-bundle.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/solution-bundle.types.js +17 -0
- package/dist/cli/backend/src/types/solution-bundle.types.js.map +1 -0
- package/dist/cli/backend/src/types/team-template.types.d.ts +8 -0
- package/dist/cli/backend/src/types/team-template.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/team-template.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts +37 -0
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.js +33 -0
- package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/cli/cli/src/commands/deploy-bundle.d.ts +160 -0
- package/dist/cli/cli/src/commands/deploy-bundle.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/deploy-bundle.js +386 -0
- package/dist/cli/cli/src/commands/deploy-bundle.js.map +1 -0
- package/dist/cli/cli/src/commands/onboard.d.ts +32 -1
- package/dist/cli/cli/src/commands/onboard.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/onboard.js +80 -3
- package/dist/cli/cli/src/commands/onboard.js.map +1 -1
- package/dist/cli/cli/src/constants.d.ts +5 -0
- package/dist/cli/cli/src/constants.d.ts.map +1 -1
- package/dist/cli/cli/src/constants.js +5 -0
- package/dist/cli/cli/src/constants.js.map +1 -1
- package/dist/cli/cli/src/index.js +15 -0
- package/dist/cli/cli/src/index.js.map +1 -1
- package/frontend/dist/assets/{index-e6c403b6.js → index-2c7ee4a6.js} +276 -276
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solution bundle apply engine.
|
|
3
|
+
*
|
|
4
|
+
* Deploys a bundle template in one step, with progress:
|
|
5
|
+
*
|
|
6
|
+
* 1. `team` — create the team(s) with filled prompts (StorageService)
|
|
7
|
+
* 2. `norms` — write norms, review points and SOPs where the team's
|
|
8
|
+
* norms and SOPs live (`teams/<id>/norms`, `teams/<id>/sops`)
|
|
9
|
+
* 3. `skills` — install missing skills (marketplace, behind an interface)
|
|
10
|
+
* 4. `connectors` — report services not yet connected, with /connections links
|
|
11
|
+
* 5. `slack` — team channels + extra channels (SlackTeamChannelService),
|
|
12
|
+
* only when Slack is connected; otherwise pending
|
|
13
|
+
* 6. `schedules` — recurring tasks (CronTaskService)
|
|
14
|
+
* 7. `first_week` — day-0 tasks go to the orchestrator now (the chat path the
|
|
15
|
+
* onboarding first task uses); later days are delivered by
|
|
16
|
+
* {@link BundleApplyService.deliverDue}
|
|
17
|
+
*
|
|
18
|
+
* Idempotent and resumable: the deployment is persisted after every step;
|
|
19
|
+
* a re-run skips steps that are done, and each step skips items it already
|
|
20
|
+
* did (existing teams are kept, sent tasks are not re-sent, schedules and
|
|
21
|
+
* channels are recorded). One failing step never stops the others, except
|
|
22
|
+
* that everything needs the team.
|
|
23
|
+
*
|
|
24
|
+
* All collaborators are injected (`bundle-apply.factory.ts` wires the
|
|
25
|
+
* backend's; the CLI wires an offline set), so the engine is testable with
|
|
26
|
+
* plain fakes and works with or without a running backend.
|
|
27
|
+
*
|
|
28
|
+
* @module services/bundle/bundle-apply.service
|
|
29
|
+
*/
|
|
30
|
+
import { randomUUID } from 'crypto';
|
|
31
|
+
import { promises as fs } from 'fs';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import { BUNDLE_CONSTANTS, RUNTIME_TYPES } from '../../constants.js';
|
|
34
|
+
import { atomicWriteFile } from '../../utils/file-io.utils.js';
|
|
35
|
+
import { bundleTeams, parseMemberRef } from './bundle-manifest.js';
|
|
36
|
+
import { BundleAnswersError, fillPlaceholders, resolveAnswers, } from './bundle-placeholders.js';
|
|
37
|
+
import { buildBundleTeam, bundleTeamId, memberForRole, memberSession, teamPlaceholderValues } from './bundle-team.builder.js';
|
|
38
|
+
import { firstWeekDueAt, isValidTimezone } from './bundle-time.js';
|
|
39
|
+
/** A request the engine refuses before anything is written. */
|
|
40
|
+
export class BundleError extends Error {
|
|
41
|
+
code;
|
|
42
|
+
details;
|
|
43
|
+
/**
|
|
44
|
+
* @param code - Machine-readable code (mapped to an HTTP status by the controller)
|
|
45
|
+
* @param message - Owner-facing message
|
|
46
|
+
* @param details - Missing / invalid answers for `invalid_answers`
|
|
47
|
+
*/
|
|
48
|
+
constructor(code, message, details) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.details = details;
|
|
52
|
+
this.name = 'BundleError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Helpers
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Error text.
|
|
60
|
+
*
|
|
61
|
+
* @param error - Thrown value
|
|
62
|
+
* @returns Message
|
|
63
|
+
*/
|
|
64
|
+
function errorText(error) {
|
|
65
|
+
return error instanceof Error ? error.message : String(error);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A fresh step list (all queued).
|
|
69
|
+
*
|
|
70
|
+
* @returns Steps in run order
|
|
71
|
+
*/
|
|
72
|
+
export function initialSteps() {
|
|
73
|
+
return BUNDLE_CONSTANTS.STEP_ORDER.map((id) => ({
|
|
74
|
+
id,
|
|
75
|
+
label: BUNDLE_CONSTANTS.STEP_LABELS[id] ?? id,
|
|
76
|
+
status: 'queued',
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The overall status from the steps.
|
|
81
|
+
*
|
|
82
|
+
* @param steps - Steps
|
|
83
|
+
* @returns `done` when everything is done (or had nothing to do), `failed`
|
|
84
|
+
* when the team could not be created, else `partial`
|
|
85
|
+
*/
|
|
86
|
+
export function overallStatus(steps) {
|
|
87
|
+
const team = steps.find((s) => s.id === BUNDLE_CONSTANTS.STEP_IDS.TEAM);
|
|
88
|
+
if (team?.status === 'failed')
|
|
89
|
+
return 'failed';
|
|
90
|
+
const finished = steps.every((s) => s.status === 'done' || (s.status === 'skipped' && s.reason === BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO));
|
|
91
|
+
return finished ? 'done' : 'partial';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Markdown with YAML frontmatter, the shape get-team-norms / get-sops read.
|
|
95
|
+
*
|
|
96
|
+
* @param fields - Frontmatter fields (undefined values are dropped)
|
|
97
|
+
* @param body - Markdown body
|
|
98
|
+
* @returns File content
|
|
99
|
+
*/
|
|
100
|
+
export function withFrontmatter(fields, body) {
|
|
101
|
+
const lines = Object.entries(fields)
|
|
102
|
+
.filter((e) => typeof e[1] === 'string' && e[1] !== '')
|
|
103
|
+
.map(([k, v]) => `${k}: ${v.replace(/\r?\n/g, ' ')}`);
|
|
104
|
+
return `---\n${lines.join('\n')}\n---\n\n${body.trim()}\n`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The message a first-week task reaches the orchestrator as.
|
|
108
|
+
*
|
|
109
|
+
* @param task - Task state
|
|
110
|
+
* @param team - Team it is for
|
|
111
|
+
* @param memberName - Member it is for
|
|
112
|
+
* @param body - Filled task text
|
|
113
|
+
* @returns Message content
|
|
114
|
+
*/
|
|
115
|
+
export function buildFirstWeekMessage(task, team, memberName, body) {
|
|
116
|
+
return [
|
|
117
|
+
`${BUNDLE_CONSTANTS.FIRST_WEEK_HEADER} 第 ${task.day + 1} 天 · ${task.title}`,
|
|
118
|
+
`请交给团队「${team.name}」(team id: ${team.id}) 的 ${memberName} 来做;团队还没启动的话先启动它。`,
|
|
119
|
+
'',
|
|
120
|
+
body.trim(),
|
|
121
|
+
].join('\n');
|
|
122
|
+
}
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Service
|
|
125
|
+
// =============================================================================
|
|
126
|
+
/**
|
|
127
|
+
* The apply engine. See the module docs.
|
|
128
|
+
*/
|
|
129
|
+
export class BundleApplyService {
|
|
130
|
+
deps;
|
|
131
|
+
/** Deployments being applied right now, by template id */
|
|
132
|
+
running = new Map();
|
|
133
|
+
/** Starts still validating / preparing, by template id (so a double tap joins them) */
|
|
134
|
+
starting = new Map();
|
|
135
|
+
/** Serialises writes per template */
|
|
136
|
+
writes = new Map();
|
|
137
|
+
/**
|
|
138
|
+
* @param deps - Collaborators
|
|
139
|
+
*/
|
|
140
|
+
constructor(deps) {
|
|
141
|
+
this.deps = deps;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate a request and start applying it in the background.
|
|
145
|
+
*
|
|
146
|
+
* A second call while the same template is being applied returns that job
|
|
147
|
+
* (a double tap on a phone must not deploy twice). Answers left out reuse
|
|
148
|
+
* the ones stored by an earlier run.
|
|
149
|
+
*
|
|
150
|
+
* @param request - Template id, answers, runtime
|
|
151
|
+
* @returns The deployment snapshot with its `jobId`
|
|
152
|
+
* @throws BundleError `unknown_bundle` | `bundle_not_ready` | `invalid_runtime` | `invalid_answers`
|
|
153
|
+
*/
|
|
154
|
+
async start(request) {
|
|
155
|
+
return (await this.begin(request)).deployment;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Apply and wait for the result (CLI, tests).
|
|
159
|
+
*
|
|
160
|
+
* @param request - Template id, answers, runtime
|
|
161
|
+
* @returns The finished deployment
|
|
162
|
+
* @throws BundleError like {@link start}
|
|
163
|
+
*/
|
|
164
|
+
async applyAndWait(request) {
|
|
165
|
+
return (await this.begin(request)).done;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Progress of a job: the live deployment while it runs, else the stored one.
|
|
169
|
+
*
|
|
170
|
+
* @param jobId - Job id from {@link start}
|
|
171
|
+
* @returns The deployment
|
|
172
|
+
* @throws BundleError `job_not_found`
|
|
173
|
+
*/
|
|
174
|
+
async getJob(jobId) {
|
|
175
|
+
for (const run of this.running.values()) {
|
|
176
|
+
if (run.deployment.jobId === jobId)
|
|
177
|
+
return run.deployment;
|
|
178
|
+
}
|
|
179
|
+
const stored = (await this.deps.store.list()).find((d) => d.jobId === jobId);
|
|
180
|
+
if (!stored)
|
|
181
|
+
throw new BundleError('job_not_found', `Job "${jobId}" not found`);
|
|
182
|
+
return stored;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The deployment of a template on this machine.
|
|
186
|
+
*
|
|
187
|
+
* @param templateId - Template id
|
|
188
|
+
* @returns The deployment, or null when it was never applied
|
|
189
|
+
*/
|
|
190
|
+
async getDeployment(templateId) {
|
|
191
|
+
return this.running.get(templateId)?.deployment ?? (await this.deps.store.read(templateId));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Deliver first-week tasks that are due (the backend calls this on a timer).
|
|
195
|
+
*
|
|
196
|
+
* @returns Number of tasks sent
|
|
197
|
+
*/
|
|
198
|
+
async deliverDue() {
|
|
199
|
+
const orchestrator = this.deps.orchestrator;
|
|
200
|
+
if (!orchestrator)
|
|
201
|
+
return 0;
|
|
202
|
+
let sent = 0;
|
|
203
|
+
for (const stored of await this.deps.store.list()) {
|
|
204
|
+
if (this.running.has(stored.templateId))
|
|
205
|
+
continue;
|
|
206
|
+
const now = this.deps.now();
|
|
207
|
+
const due = stored.firstWeek.filter((t) => t.status === 'scheduled' && Date.parse(t.dueAt) <= now.getTime());
|
|
208
|
+
if (due.length === 0)
|
|
209
|
+
continue;
|
|
210
|
+
const entry = this.deps.catalog.get(stored.templateId);
|
|
211
|
+
if (!entry)
|
|
212
|
+
continue;
|
|
213
|
+
const ctx = this.contextFor(entry, stored, stored.answers);
|
|
214
|
+
for (const task of due) {
|
|
215
|
+
if (await this.sendFirstWeekTask(ctx, task))
|
|
216
|
+
sent += 1;
|
|
217
|
+
}
|
|
218
|
+
const step = stored.steps.find((s) => s.id === BUNDLE_CONSTANTS.STEP_IDS.FIRST_WEEK);
|
|
219
|
+
if (step)
|
|
220
|
+
Object.assign(step, this.firstWeekOutcome(stored));
|
|
221
|
+
stored.status = overallStatus(stored.steps);
|
|
222
|
+
stored.updatedAt = this.deps.now().toISOString();
|
|
223
|
+
await this.persist(stored);
|
|
224
|
+
}
|
|
225
|
+
return sent;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Re-run deployments that were waiting on something that is now there:
|
|
229
|
+
* the backend (after a CLI deploy without one) or Slack.
|
|
230
|
+
*
|
|
231
|
+
* @returns Template ids that were restarted
|
|
232
|
+
*/
|
|
233
|
+
async resumeWaiting() {
|
|
234
|
+
const restarted = [];
|
|
235
|
+
for (const stored of await this.deps.store.list()) {
|
|
236
|
+
if (stored.status === 'done' || this.running.has(stored.templateId))
|
|
237
|
+
continue;
|
|
238
|
+
const resumable = stored.steps.some((s) => s.status === 'pending' &&
|
|
239
|
+
(s.reason === BUNDLE_CONSTANTS.PENDING_REASONS.BACKEND_NOT_RUNNING ||
|
|
240
|
+
(s.reason === BUNDLE_CONSTANTS.PENDING_REASONS.SLACK_NOT_CONNECTED && !!this.deps.slack?.isConnected())));
|
|
241
|
+
if (!resumable || !this.deps.catalog.get(stored.templateId))
|
|
242
|
+
continue;
|
|
243
|
+
try {
|
|
244
|
+
await this.start({ templateId: stored.templateId, allowDraft: true });
|
|
245
|
+
restarted.push(stored.templateId);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// A template that no longer validates stays as it is.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return restarted;
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Run
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
/**
|
|
257
|
+
* Validate, prepare the deployment and start the run.
|
|
258
|
+
*
|
|
259
|
+
* @param request - Request
|
|
260
|
+
* @returns The deployment and a promise of the finished one
|
|
261
|
+
*/
|
|
262
|
+
async begin(request) {
|
|
263
|
+
const inflight = this.running.get(request.templateId);
|
|
264
|
+
if (inflight)
|
|
265
|
+
return inflight;
|
|
266
|
+
const pending = this.starting.get(request.templateId);
|
|
267
|
+
if (pending)
|
|
268
|
+
return pending;
|
|
269
|
+
const prepared = this.prepare(request).finally(() => {
|
|
270
|
+
this.starting.delete(request.templateId);
|
|
271
|
+
});
|
|
272
|
+
this.starting.set(request.templateId, prepared);
|
|
273
|
+
return prepared;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Validate the request, build and persist the deployment, start the run.
|
|
277
|
+
*
|
|
278
|
+
* @param request - Request
|
|
279
|
+
* @returns The deployment and a promise of the finished one
|
|
280
|
+
* @throws BundleError for a refused request
|
|
281
|
+
*/
|
|
282
|
+
async prepare(request) {
|
|
283
|
+
const entry = this.deps.catalog.get(request.templateId);
|
|
284
|
+
if (!entry)
|
|
285
|
+
throw new BundleError('unknown_bundle', `没有找到方案「${request.templateId}」`);
|
|
286
|
+
const { template } = entry;
|
|
287
|
+
const draft = (template.bundle.status ?? BUNDLE_CONSTANTS.STATUS.READY) !== BUNDLE_CONSTANTS.STATUS.READY;
|
|
288
|
+
if (draft && !request.allowDraft) {
|
|
289
|
+
throw new BundleError('bundle_not_ready', `方案「${template.bundle.label}」还在准备中,暂时不能部署`);
|
|
290
|
+
}
|
|
291
|
+
const runtimes = Object.values(RUNTIME_TYPES);
|
|
292
|
+
if (request.runtime !== undefined && !runtimes.includes(request.runtime)) {
|
|
293
|
+
throw new BundleError('invalid_runtime', `Unknown runtime "${request.runtime}" (use ${runtimes.join(', ')})`);
|
|
294
|
+
}
|
|
295
|
+
const previous = await this.deps.store.read(template.id);
|
|
296
|
+
const rawAnswers = request.answers ?? previous?.answers ?? {};
|
|
297
|
+
let answers;
|
|
298
|
+
try {
|
|
299
|
+
answers = resolveAnswers(template.bundle.questions ?? [], rawAnswers);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error instanceof BundleAnswersError) {
|
|
303
|
+
throw new BundleError('invalid_answers', error.message, { missing: error.missing, invalid: error.invalid });
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
const now = this.deps.now().toISOString();
|
|
308
|
+
const teamDone = previous?.steps.some((s) => s.id === BUNDLE_CONSTANTS.STEP_IDS.TEAM && s.status === 'done') ?? false;
|
|
309
|
+
const runtime = teamDone && previous
|
|
310
|
+
? previous.runtime
|
|
311
|
+
: await this.deps.resolveRuntime(template.bundle.runtime.recommended, request.runtime);
|
|
312
|
+
const jobId = this.deps.newJobId?.() ?? `bundle-${randomUUID()}`;
|
|
313
|
+
const steps = initialSteps().map((fresh) => {
|
|
314
|
+
const old = previous?.steps.find((s) => s.id === fresh.id);
|
|
315
|
+
return old && old.status === 'done' ? old : fresh;
|
|
316
|
+
});
|
|
317
|
+
const deployment = {
|
|
318
|
+
templateId: template.id,
|
|
319
|
+
templateVersion: template.version ?? '0.0.0',
|
|
320
|
+
jobId,
|
|
321
|
+
status: 'running',
|
|
322
|
+
runtime,
|
|
323
|
+
answers: teamDone && previous ? previous.answers : answers,
|
|
324
|
+
startedAt: now,
|
|
325
|
+
updatedAt: now,
|
|
326
|
+
teams: previous?.teams ?? [],
|
|
327
|
+
steps,
|
|
328
|
+
connectors: previous?.connectors ?? [],
|
|
329
|
+
firstWeek: previous?.firstWeek ?? [],
|
|
330
|
+
schedules: previous?.schedules ?? {},
|
|
331
|
+
channels: previous?.channels ?? {},
|
|
332
|
+
};
|
|
333
|
+
await this.persist(deployment);
|
|
334
|
+
const ctx = this.contextFor(entry, deployment, deployment.answers);
|
|
335
|
+
const done = this.run(ctx).finally(() => {
|
|
336
|
+
this.running.delete(template.id);
|
|
337
|
+
});
|
|
338
|
+
const handle = { deployment, done };
|
|
339
|
+
this.running.set(template.id, handle);
|
|
340
|
+
return handle;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Shared context for a deployment.
|
|
344
|
+
*
|
|
345
|
+
* @param entry - Loaded bundle
|
|
346
|
+
* @param deployment - Deployment
|
|
347
|
+
* @param answers - Answers
|
|
348
|
+
* @returns Context
|
|
349
|
+
*/
|
|
350
|
+
contextFor(entry, deployment, answers) {
|
|
351
|
+
return { entry, template: entry.template, teams: bundleTeams(entry.template), deployment, answers };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Run every step that is not done, persisting after each.
|
|
355
|
+
*
|
|
356
|
+
* @param ctx - Context
|
|
357
|
+
* @returns The finished deployment
|
|
358
|
+
*/
|
|
359
|
+
async run(ctx) {
|
|
360
|
+
const { deployment } = ctx;
|
|
361
|
+
const bodies = {
|
|
362
|
+
[BUNDLE_CONSTANTS.STEP_IDS.TEAM]: (c) => this.stepTeam(c),
|
|
363
|
+
[BUNDLE_CONSTANTS.STEP_IDS.NORMS]: (c) => this.stepNorms(c),
|
|
364
|
+
[BUNDLE_CONSTANTS.STEP_IDS.SKILLS]: (c) => this.stepSkills(c),
|
|
365
|
+
[BUNDLE_CONSTANTS.STEP_IDS.CONNECTORS]: (c) => this.stepConnectors(c),
|
|
366
|
+
[BUNDLE_CONSTANTS.STEP_IDS.SLACK]: (c) => this.stepSlack(c),
|
|
367
|
+
[BUNDLE_CONSTANTS.STEP_IDS.SCHEDULES]: (c) => this.stepSchedules(c),
|
|
368
|
+
[BUNDLE_CONSTANTS.STEP_IDS.FIRST_WEEK]: (c) => this.stepFirstWeek(c),
|
|
369
|
+
};
|
|
370
|
+
const needsTeam = new Set([
|
|
371
|
+
BUNDLE_CONSTANTS.STEP_IDS.NORMS,
|
|
372
|
+
BUNDLE_CONSTANTS.STEP_IDS.SLACK,
|
|
373
|
+
BUNDLE_CONSTANTS.STEP_IDS.SCHEDULES,
|
|
374
|
+
BUNDLE_CONSTANTS.STEP_IDS.FIRST_WEEK,
|
|
375
|
+
]);
|
|
376
|
+
for (const step of deployment.steps) {
|
|
377
|
+
if (step.status === 'done')
|
|
378
|
+
continue;
|
|
379
|
+
const teamStep = deployment.steps.find((s) => s.id === BUNDLE_CONSTANTS.STEP_IDS.TEAM);
|
|
380
|
+
if (needsTeam.has(step.id) && teamStep?.status !== 'done') {
|
|
381
|
+
this.finishStep(step, { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.TEAM_FAILED, message: '团队没建好,这一步先跳过' });
|
|
382
|
+
await this.touch(deployment);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
step.status = 'running';
|
|
386
|
+
step.startedAt = this.deps.now().toISOString();
|
|
387
|
+
delete step.error;
|
|
388
|
+
delete step.reason;
|
|
389
|
+
await this.touch(deployment);
|
|
390
|
+
let outcome;
|
|
391
|
+
try {
|
|
392
|
+
outcome = await bodies[step.id](ctx);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
outcome = { status: 'failed', error: errorText(error), message: '这一步出错了,重新部署会从这里接着做' };
|
|
396
|
+
}
|
|
397
|
+
this.finishStep(step, outcome);
|
|
398
|
+
await this.touch(deployment);
|
|
399
|
+
}
|
|
400
|
+
deployment.status = overallStatus(deployment.steps);
|
|
401
|
+
deployment.finishedAt = this.deps.now().toISOString();
|
|
402
|
+
await this.touch(deployment);
|
|
403
|
+
return deployment;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Apply an outcome to a step.
|
|
407
|
+
*
|
|
408
|
+
* @param step - Step
|
|
409
|
+
* @param outcome - Outcome
|
|
410
|
+
*/
|
|
411
|
+
finishStep(step, outcome) {
|
|
412
|
+
step.status = outcome.status;
|
|
413
|
+
step.finishedAt = this.deps.now().toISOString();
|
|
414
|
+
if (outcome.reason)
|
|
415
|
+
step.reason = outcome.reason;
|
|
416
|
+
else
|
|
417
|
+
delete step.reason;
|
|
418
|
+
if (outcome.message)
|
|
419
|
+
step.message = outcome.message;
|
|
420
|
+
else
|
|
421
|
+
delete step.message;
|
|
422
|
+
if (outcome.error)
|
|
423
|
+
step.error = outcome.error;
|
|
424
|
+
else
|
|
425
|
+
delete step.error;
|
|
426
|
+
if (outcome.items)
|
|
427
|
+
step.items = outcome.items;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Stamp and persist.
|
|
431
|
+
*
|
|
432
|
+
* @param deployment - Deployment
|
|
433
|
+
*/
|
|
434
|
+
async touch(deployment) {
|
|
435
|
+
deployment.updatedAt = this.deps.now().toISOString();
|
|
436
|
+
await this.persist(deployment);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Persist a deployment; writes of one template never overlap.
|
|
440
|
+
*
|
|
441
|
+
* @param deployment - Deployment
|
|
442
|
+
*/
|
|
443
|
+
async persist(deployment) {
|
|
444
|
+
const prev = this.writes.get(deployment.templateId) ?? Promise.resolve();
|
|
445
|
+
const snapshot = JSON.parse(JSON.stringify(deployment));
|
|
446
|
+
const next = prev.catch(() => undefined).then(() => this.deps.store.write(snapshot));
|
|
447
|
+
this.writes.set(deployment.templateId, next);
|
|
448
|
+
await next;
|
|
449
|
+
}
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Team lookups shared by the steps
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
/**
|
|
454
|
+
* The saved team for a bundle team key.
|
|
455
|
+
*
|
|
456
|
+
* @param ctx - Context
|
|
457
|
+
* @param key - Team key
|
|
458
|
+
* @returns The team
|
|
459
|
+
* @throws Error when it is missing (deleted since the team step)
|
|
460
|
+
*/
|
|
461
|
+
async savedTeam(ctx, key) {
|
|
462
|
+
const id = ctx.deployment.teams.find((t) => t.key === key)?.teamId ?? bundleTeamId(ctx.template.id, key);
|
|
463
|
+
const team = await this.deps.teams.get(id);
|
|
464
|
+
if (!team)
|
|
465
|
+
throw new Error(`Team "${id}" no longer exists; delete ${this.deps.store.fileFor(ctx.template.id)} to deploy again`);
|
|
466
|
+
return team;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Placeholder values of a saved team.
|
|
470
|
+
*
|
|
471
|
+
* @param ctx - Context
|
|
472
|
+
* @param team - Saved team
|
|
473
|
+
* @returns Values (answers + team_name + lead_name)
|
|
474
|
+
*/
|
|
475
|
+
valuesFor(ctx, team) {
|
|
476
|
+
const lead = team.members.find((m) => m.id === team.leaderId) ?? team.members[0];
|
|
477
|
+
return teamPlaceholderValues(ctx.answers, team.name, lead?.name ?? '');
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Resolve a member ref (default: the main team's lead).
|
|
481
|
+
*
|
|
482
|
+
* @param ctx - Context
|
|
483
|
+
* @param ref - `role` / `team/role`, or undefined
|
|
484
|
+
* @returns Team and member session / name
|
|
485
|
+
*/
|
|
486
|
+
async resolveRef(ctx, ref) {
|
|
487
|
+
const main = ctx.teams[0];
|
|
488
|
+
const parsed = ref ? parseMemberRef(ref) : { teamKey: main.key, role: main.leadRole };
|
|
489
|
+
const team = await this.savedTeam(ctx, parsed.teamKey);
|
|
490
|
+
const member = memberForRole(team, parsed.role);
|
|
491
|
+
if (!member)
|
|
492
|
+
throw new Error(`No member with role "${parsed.role}" in team "${team.name}"`);
|
|
493
|
+
return { team, session: memberSession(member), memberName: member.name };
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Steps
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
/** Step `team`: create every team that does not exist yet. */
|
|
499
|
+
async stepTeam(ctx) {
|
|
500
|
+
const items = [];
|
|
501
|
+
const deployed = [];
|
|
502
|
+
for (const spec of ctx.teams) {
|
|
503
|
+
const id = bundleTeamId(ctx.template.id, spec.key);
|
|
504
|
+
const existing = await this.deps.teams.get(id);
|
|
505
|
+
if (existing) {
|
|
506
|
+
deployed.push({ key: spec.key, teamId: id, name: existing.name });
|
|
507
|
+
items.push({ id: spec.key, label: existing.name, status: 'done', message: '已存在,保留原样' });
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const team = buildBundleTeam({ template: ctx.template, team: spec, answers: ctx.answers, runtime: ctx.deployment.runtime, now: this.deps.now() });
|
|
511
|
+
await this.deps.teams.save(team);
|
|
512
|
+
deployed.push({ key: spec.key, teamId: team.id, name: team.name });
|
|
513
|
+
items.push({ id: spec.key, label: team.name, status: 'done', message: `${team.members.length} 位成员` });
|
|
514
|
+
}
|
|
515
|
+
ctx.deployment.teams = deployed;
|
|
516
|
+
const names = deployed.map((t) => `「${t.name}」`).join('、');
|
|
517
|
+
return { status: 'done', items, message: `团队 ${names} 已就绪` };
|
|
518
|
+
}
|
|
519
|
+
/** Step `norms`: norms, review points and SOPs into the teams' folders. */
|
|
520
|
+
async stepNorms(ctx) {
|
|
521
|
+
const b = ctx.template.bundle;
|
|
522
|
+
const norms = b.norms ?? [];
|
|
523
|
+
const sops = b.sops ?? [];
|
|
524
|
+
const reviews = b.reviewPoints ?? [];
|
|
525
|
+
if (norms.length + sops.length + reviews.length === 0) {
|
|
526
|
+
return { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO, message: '这个方案没有额外规范' };
|
|
527
|
+
}
|
|
528
|
+
const items = [];
|
|
529
|
+
const main = BUNDLE_CONSTANTS.MAIN_TEAM_KEY;
|
|
530
|
+
const updatedBy = `bundle:${ctx.template.id}`;
|
|
531
|
+
const updatedAt = this.deps.now().toISOString();
|
|
532
|
+
for (const spec of ctx.teams) {
|
|
533
|
+
const team = await this.savedTeam(ctx, spec.key);
|
|
534
|
+
const values = this.valuesFor(ctx, team);
|
|
535
|
+
const teamDir = path.join(this.deps.crewlyHome, 'teams', team.id);
|
|
536
|
+
for (const norm of norms.filter((n) => (n.team ?? main) === spec.key)) {
|
|
537
|
+
const body = fillPlaceholders(await this.docBody(ctx, norm), values, `norm ${norm.id}`);
|
|
538
|
+
const file = path.join(teamDir, BUNDLE_CONSTANTS.NORMS_DIR, `${norm.id}.md`);
|
|
539
|
+
await this.writeDoc(file, withFrontmatter({ title: fillPlaceholders(norm.title, values, `norm ${norm.id} title`), trigger: norm.trigger, updatedBy, updatedAt }, body));
|
|
540
|
+
items.push({ id: `norm:${spec.key}/${norm.id}`, label: norm.title, status: 'done' });
|
|
541
|
+
}
|
|
542
|
+
const teamReviews = reviews.filter((r) => (r.team ?? main) === spec.key);
|
|
543
|
+
if (teamReviews.length > 0) {
|
|
544
|
+
const rows = teamReviews.map((r) => {
|
|
545
|
+
const who = r.approver === 'owner' ? '老板' : `负责人 ${values.lead_name}`;
|
|
546
|
+
const how = r.how ? `:${fillPlaceholders(r.how, values, `review ${r.id}`)}` : '';
|
|
547
|
+
return `- **${fillPlaceholders(r.what, values, `review ${r.id}`)}** → 先拿到${who}的同意${how}`;
|
|
548
|
+
});
|
|
549
|
+
const body = [
|
|
550
|
+
'这些事必须先拿到同意才能做。没得到明确的「可以」之前,只准备、不执行。',
|
|
551
|
+
'',
|
|
552
|
+
...rows,
|
|
553
|
+
'',
|
|
554
|
+
'拿不准算不算的时候,按「需要同意」处理。',
|
|
555
|
+
].join('\n');
|
|
556
|
+
const file = path.join(teamDir, BUNDLE_CONSTANTS.NORMS_DIR, `${BUNDLE_CONSTANTS.REVIEW_POINTS_NORM_ID}.md`);
|
|
557
|
+
await this.writeDoc(file, withFrontmatter({ title: '需要老板点头的事', trigger: 'before_publish,approval,owner_review,external', updatedBy, updatedAt }, body));
|
|
558
|
+
items.push({ id: `norm:${spec.key}/${BUNDLE_CONSTANTS.REVIEW_POINTS_NORM_ID}`, label: '需要老板点头的事', status: 'done' });
|
|
559
|
+
}
|
|
560
|
+
for (const sop of sops.filter((s) => (s.team ?? main) === spec.key)) {
|
|
561
|
+
const category = sop.category ?? BUNDLE_CONSTANTS.DEFAULT_SOP_CATEGORY;
|
|
562
|
+
const body = fillPlaceholders(await this.docBody(ctx, sop), values, `sop ${sop.id}`);
|
|
563
|
+
const file = path.join(teamDir, BUNDLE_CONSTANTS.SOPS_DIR, category, `${sop.id}.md`);
|
|
564
|
+
await this.writeDoc(file, withFrontmatter({ title: fillPlaceholders(sop.title, values, `sop ${sop.id} title`), category, updatedBy, updatedAt }, body));
|
|
565
|
+
items.push({ id: `sop:${spec.key}/${sop.id}`, label: sop.title, status: 'done' });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { status: 'done', items, message: `写好 ${items.length} 份规范和 SOP` };
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The body of a norm / SOP: inline content or the referenced file.
|
|
572
|
+
*
|
|
573
|
+
* @param ctx - Context
|
|
574
|
+
* @param doc - Norm or SOP
|
|
575
|
+
* @returns Markdown body (unfilled)
|
|
576
|
+
*/
|
|
577
|
+
async docBody(ctx, doc) {
|
|
578
|
+
if (doc.content !== undefined)
|
|
579
|
+
return doc.content;
|
|
580
|
+
return fs.readFile(path.join(ctx.entry.dir, doc.file), 'utf-8');
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Write a file atomically, creating its folder.
|
|
584
|
+
*
|
|
585
|
+
* @param file - Absolute path
|
|
586
|
+
* @param content - Content
|
|
587
|
+
*/
|
|
588
|
+
async writeDoc(file, content) {
|
|
589
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
590
|
+
await atomicWriteFile(file, content);
|
|
591
|
+
}
|
|
592
|
+
/** Step `skills`: install what is missing. */
|
|
593
|
+
async stepSkills(ctx) {
|
|
594
|
+
const required = ctx.template.bundle.skills?.required ?? [];
|
|
595
|
+
const optional = ctx.template.bundle.skills?.optional ?? [];
|
|
596
|
+
if (required.length + optional.length === 0) {
|
|
597
|
+
return { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO, message: '不需要额外技能' };
|
|
598
|
+
}
|
|
599
|
+
const items = [];
|
|
600
|
+
let requiredFailed = 0;
|
|
601
|
+
for (const [skillId, isRequired] of [...required.map((s) => [s, true]), ...optional.map((s) => [s, false])]) {
|
|
602
|
+
try {
|
|
603
|
+
if (await this.deps.skills.isAvailable(skillId)) {
|
|
604
|
+
items.push({ id: skillId, label: skillId, status: 'done', message: '已有' });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const result = await this.deps.skills.install(skillId);
|
|
608
|
+
if (result.ok)
|
|
609
|
+
items.push({ id: skillId, label: skillId, status: 'done', message: result.message });
|
|
610
|
+
else {
|
|
611
|
+
items.push({ id: skillId, label: skillId, status: 'failed', message: result.message });
|
|
612
|
+
if (isRequired)
|
|
613
|
+
requiredFailed += 1;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
items.push({ id: skillId, label: skillId, status: 'failed', message: errorText(error) });
|
|
618
|
+
if (isRequired)
|
|
619
|
+
requiredFailed += 1;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (requiredFailed > 0) {
|
|
623
|
+
return { status: 'failed', items, error: `${requiredFailed} 个必需技能没装上`, message: '有技能没装上,重新部署会再试' };
|
|
624
|
+
}
|
|
625
|
+
const optionalFailed = items.filter((i) => i.status === 'failed').length;
|
|
626
|
+
return { status: 'done', items, message: optionalFailed > 0 ? `技能已就绪(${optionalFailed} 个可选技能没装上)` : '技能已就绪' };
|
|
627
|
+
}
|
|
628
|
+
/** Step `connectors`: which services still need connecting. */
|
|
629
|
+
async stepConnectors(ctx) {
|
|
630
|
+
const specs = ctx.template.bundle.connectors ?? [];
|
|
631
|
+
if (specs.length === 0) {
|
|
632
|
+
return { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO, message: '不需要接别的服务' };
|
|
633
|
+
}
|
|
634
|
+
const states = [];
|
|
635
|
+
for (const spec of specs) {
|
|
636
|
+
let status = 'unknown';
|
|
637
|
+
if (this.deps.connectors) {
|
|
638
|
+
try {
|
|
639
|
+
status = await this.deps.connectors.check(spec);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
status = 'unknown';
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
states.push({
|
|
646
|
+
id: spec.id,
|
|
647
|
+
products: spec.products ?? [],
|
|
648
|
+
required: spec.required,
|
|
649
|
+
why: spec.why,
|
|
650
|
+
status,
|
|
651
|
+
connectPath: `${BUNDLE_CONSTANTS.CONNECTIONS_PATH}?platform=${encodeURIComponent(spec.id)}`,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
ctx.deployment.connectors = states;
|
|
655
|
+
const items = states.map((s) => ({
|
|
656
|
+
id: s.id,
|
|
657
|
+
label: s.products.length > 0 ? `${s.id} (${s.products.join(', ')})` : s.id,
|
|
658
|
+
status: s.status === 'connected' ? 'done' : 'pending',
|
|
659
|
+
message: s.status === 'connected' ? '已连接' : `${s.required ? '需要连接' : '建议连接'}:${s.why} → ${s.connectPath}`,
|
|
660
|
+
}));
|
|
661
|
+
if (!this.deps.connectors) {
|
|
662
|
+
return { status: 'pending', reason: BUNDLE_CONSTANTS.PENDING_REASONS.BACKEND_NOT_RUNNING, items, message: 'Crewly 启动后再检查;先在 /connections 连好这些服务' };
|
|
663
|
+
}
|
|
664
|
+
const missing = states.filter((s) => s.required && s.status !== 'connected');
|
|
665
|
+
if (missing.length > 0) {
|
|
666
|
+
return {
|
|
667
|
+
status: 'pending',
|
|
668
|
+
reason: BUNDLE_CONSTANTS.PENDING_REASONS.CONNECTORS_MISSING,
|
|
669
|
+
items,
|
|
670
|
+
message: `还要连接:${missing.map((m) => m.id).join('、')}(在「连接」页面,手机上也能点)`,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
return { status: 'done', items, message: '要用的服务都连好了' };
|
|
674
|
+
}
|
|
675
|
+
/** Step `slack`: team channels and extra channels. */
|
|
676
|
+
async stepSlack(ctx) {
|
|
677
|
+
const layout = ctx.template.bundle.slack ?? {};
|
|
678
|
+
const teamChannels = layout.teamChannels !== false;
|
|
679
|
+
const extra = layout.channels ?? [];
|
|
680
|
+
if (!teamChannels && extra.length === 0) {
|
|
681
|
+
return { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO, message: '这个方案不用 Slack 频道' };
|
|
682
|
+
}
|
|
683
|
+
const slack = this.deps.slack;
|
|
684
|
+
if (!slack) {
|
|
685
|
+
return { status: 'pending', reason: BUNDLE_CONSTANTS.PENDING_REASONS.BACKEND_NOT_RUNNING, message: 'Crewly 启动并连上 Slack 后自动建频道' };
|
|
686
|
+
}
|
|
687
|
+
if (!slack.isConnected()) {
|
|
688
|
+
return { status: 'pending', reason: BUNDLE_CONSTANTS.PENDING_REASONS.SLACK_NOT_CONNECTED, message: '还没连 Slack;连上后会自动建频道、拉成员进来' };
|
|
689
|
+
}
|
|
690
|
+
const items = [];
|
|
691
|
+
let failed = 0;
|
|
692
|
+
const allTeams = [];
|
|
693
|
+
for (const spec of ctx.teams)
|
|
694
|
+
allTeams.push(await this.savedTeam(ctx, spec.key));
|
|
695
|
+
if (teamChannels) {
|
|
696
|
+
for (const team of allTeams) {
|
|
697
|
+
try {
|
|
698
|
+
const ch = await slack.ensureTeamChannel(team);
|
|
699
|
+
items.push({ id: `team:${team.id}`, label: `#${ch.slackChannelName}`, status: 'done', message: `团队「${team.name}」的频道` });
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
failed += 1;
|
|
703
|
+
items.push({ id: `team:${team.id}`, label: team.name, status: 'failed', message: errorText(error) });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const mainValues = this.valuesFor(ctx, allTeams[0]);
|
|
708
|
+
for (const channel of extra) {
|
|
709
|
+
try {
|
|
710
|
+
const sessions = new Set();
|
|
711
|
+
for (const ref of channel.members) {
|
|
712
|
+
if (ref === '*') {
|
|
713
|
+
allTeams.forEach((t) => t.members.forEach((m) => sessions.add(memberSession(m))));
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
sessions.add((await this.resolveRef(ctx, ref)).session);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const ch = await slack.ensureAgentChannel({
|
|
720
|
+
name: fillPlaceholders(channel.name, mainValues, `channel ${channel.key}`),
|
|
721
|
+
purpose: channel.purpose ? fillPlaceholders(channel.purpose, mainValues, `channel ${channel.key} purpose`) : '',
|
|
722
|
+
memberSessions: [...sessions],
|
|
723
|
+
...(ctx.deployment.channels[channel.key] ? { existingChannelId: ctx.deployment.channels[channel.key] } : {}),
|
|
724
|
+
});
|
|
725
|
+
ctx.deployment.channels[channel.key] = ch.slackChannelId;
|
|
726
|
+
items.push({ id: `channel:${channel.key}`, label: `#${ch.slackChannelName}`, status: 'done', message: `${sessions.size} 位成员` });
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
failed += 1;
|
|
730
|
+
items.push({ id: `channel:${channel.key}`, label: channel.name, status: 'failed', message: errorText(error) });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (failed > 0)
|
|
734
|
+
return { status: 'failed', items, error: `${failed} 个频道没建好`, message: '有频道没建好,重新部署会再试' };
|
|
735
|
+
return { status: 'done', items, message: `建好 ${items.length} 个 Slack 频道` };
|
|
736
|
+
}
|
|
737
|
+
/** Step `schedules`: recurring tasks through the cron-task scheduler. */
|
|
738
|
+
async stepSchedules(ctx) {
|
|
739
|
+
const schedules = ctx.template.bundle.schedules ?? [];
|
|
740
|
+
if (schedules.length === 0) {
|
|
741
|
+
return { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO, message: '这个方案没有定时任务' };
|
|
742
|
+
}
|
|
743
|
+
const api = this.deps.schedules;
|
|
744
|
+
if (!api) {
|
|
745
|
+
return { status: 'pending', reason: BUNDLE_CONSTANTS.PENDING_REASONS.BACKEND_NOT_RUNNING, message: 'Crewly 启动后自动排定时任务' };
|
|
746
|
+
}
|
|
747
|
+
const items = [];
|
|
748
|
+
let failed = 0;
|
|
749
|
+
for (const schedule of schedules) {
|
|
750
|
+
if (ctx.deployment.schedules[schedule.id]) {
|
|
751
|
+
items.push({ id: schedule.id, label: schedule.title, status: 'done', message: '已排好' });
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const { team, session, memberName } = await this.resolveRef(ctx, schedule.target);
|
|
756
|
+
const values = this.valuesFor(ctx, team);
|
|
757
|
+
const timezone = this.timezoneFor(ctx, values, schedule.timezone);
|
|
758
|
+
const task = await api.create({
|
|
759
|
+
cronExpression: schedule.cron,
|
|
760
|
+
timezone,
|
|
761
|
+
targetAgent: session,
|
|
762
|
+
targetTeamId: team.id,
|
|
763
|
+
taskDescription: `【${schedule.title}】\n${fillPlaceholders(schedule.task, values, `schedule ${schedule.id}`)}`,
|
|
764
|
+
createdBy: 'user',
|
|
765
|
+
});
|
|
766
|
+
ctx.deployment.schedules[schedule.id] = task.id;
|
|
767
|
+
items.push({ id: schedule.id, label: schedule.title, status: 'done', message: `${schedule.cron}(${timezone})→ ${memberName}` });
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
failed += 1;
|
|
771
|
+
items.push({ id: schedule.id, label: schedule.title, status: 'failed', message: errorText(error) });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (failed > 0)
|
|
775
|
+
return { status: 'failed', items, error: `${failed} 个定时任务没排上`, message: '有定时任务没排上,重新部署会再试' };
|
|
776
|
+
return { status: 'done', items, message: `排好 ${items.length} 个定时任务` };
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* A valid timezone from a (placeholder) value, the bundle's, or the default.
|
|
780
|
+
*
|
|
781
|
+
* @param ctx - Context
|
|
782
|
+
* @param values - Placeholder values
|
|
783
|
+
* @param override - Schedule's own timezone
|
|
784
|
+
* @returns IANA timezone
|
|
785
|
+
* @throws Error when the filled timezone is not valid
|
|
786
|
+
*/
|
|
787
|
+
timezoneFor(ctx, values, override) {
|
|
788
|
+
const raw = override ?? ctx.template.bundle.timezone ?? BUNDLE_CONSTANTS.DEFAULT_TIMEZONE;
|
|
789
|
+
const tz = fillPlaceholders(raw, values, 'timezone').trim() || BUNDLE_CONSTANTS.DEFAULT_TIMEZONE;
|
|
790
|
+
if (!isValidTimezone(tz))
|
|
791
|
+
throw new Error(`Unknown timezone "${tz}"`);
|
|
792
|
+
return tz;
|
|
793
|
+
}
|
|
794
|
+
/** Step `first_week`: send today's tasks, schedule the rest. */
|
|
795
|
+
async stepFirstWeek(ctx) {
|
|
796
|
+
const tasks = ctx.template.bundle.firstWeek ?? [];
|
|
797
|
+
if (tasks.length === 0) {
|
|
798
|
+
return { status: 'skipped', reason: BUNDLE_CONSTANTS.SKIP_REASONS.NOTHING_TO_DO, message: '这个方案没有第一周任务' };
|
|
799
|
+
}
|
|
800
|
+
const deployedAt = new Date(ctx.deployment.startedAt);
|
|
801
|
+
for (const task of tasks) {
|
|
802
|
+
if (ctx.deployment.firstWeek.some((t) => t.id === task.id))
|
|
803
|
+
continue;
|
|
804
|
+
const { team, session } = await this.resolveRef(ctx, task.target);
|
|
805
|
+
const tz = this.timezoneFor(ctx, this.valuesFor(ctx, team));
|
|
806
|
+
ctx.deployment.firstWeek.push({
|
|
807
|
+
id: task.id,
|
|
808
|
+
title: task.title,
|
|
809
|
+
day: task.day,
|
|
810
|
+
dueAt: firstWeekDueAt(deployedAt, task.day, task.time, tz),
|
|
811
|
+
teamId: team.id,
|
|
812
|
+
target: session,
|
|
813
|
+
status: 'scheduled',
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
const now = this.deps.now().getTime();
|
|
817
|
+
for (const state of ctx.deployment.firstWeek) {
|
|
818
|
+
if ((state.status === 'scheduled' || state.status === 'failed') && Date.parse(state.dueAt) <= now && this.deps.orchestrator) {
|
|
819
|
+
await this.sendFirstWeekTask(ctx, state);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return this.firstWeekOutcome(ctx.deployment);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Send one first-week task through the orchestrator and record the result.
|
|
826
|
+
*
|
|
827
|
+
* @param ctx - Context
|
|
828
|
+
* @param state - Task state (updated in place)
|
|
829
|
+
* @returns True when it was handed over
|
|
830
|
+
*/
|
|
831
|
+
async sendFirstWeekTask(ctx, state) {
|
|
832
|
+
const orchestrator = this.deps.orchestrator;
|
|
833
|
+
const spec = ctx.template.bundle.firstWeek?.find((t) => t.id === state.id);
|
|
834
|
+
if (!orchestrator || !spec)
|
|
835
|
+
return false;
|
|
836
|
+
try {
|
|
837
|
+
const team = await this.deps.teams.get(state.teamId);
|
|
838
|
+
if (!team)
|
|
839
|
+
throw new Error(`Team "${state.teamId}" no longer exists`);
|
|
840
|
+
const member = team.members.find((m) => memberSession(m) === state.target);
|
|
841
|
+
const body = fillPlaceholders(spec.task, this.valuesFor(ctx, team), `first-week ${spec.id}`);
|
|
842
|
+
const result = await orchestrator.send(buildFirstWeekMessage(state, team, member?.name ?? state.target, body), {
|
|
843
|
+
source: BUNDLE_CONSTANTS.FIRST_WEEK_SOURCE,
|
|
844
|
+
templateId: ctx.template.id,
|
|
845
|
+
teamId: team.id,
|
|
846
|
+
taskId: state.id,
|
|
847
|
+
});
|
|
848
|
+
if (!result.forwarded)
|
|
849
|
+
throw new Error(result.error ?? 'The orchestrator did not take it');
|
|
850
|
+
state.status = 'sent';
|
|
851
|
+
state.sentAt = this.deps.now().toISOString();
|
|
852
|
+
state.conversationId = result.conversationId;
|
|
853
|
+
delete state.error;
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
state.status = 'failed';
|
|
858
|
+
state.error = errorText(error);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* The first-week step's state from its tasks.
|
|
864
|
+
*
|
|
865
|
+
* @param deployment - Deployment
|
|
866
|
+
* @returns Outcome
|
|
867
|
+
*/
|
|
868
|
+
firstWeekOutcome(deployment) {
|
|
869
|
+
const items = deployment.firstWeek.map((t) => ({
|
|
870
|
+
id: t.id,
|
|
871
|
+
label: `第 ${t.day + 1} 天 · ${t.title}`,
|
|
872
|
+
status: t.status === 'sent' ? 'done' : t.status === 'failed' ? 'failed' : 'pending',
|
|
873
|
+
message: t.status === 'sent' ? '已交给团队' : t.status === 'failed' ? t.error : `排在 ${t.dueAt}`,
|
|
874
|
+
}));
|
|
875
|
+
const failed = deployment.firstWeek.filter((t) => t.status === 'failed').length;
|
|
876
|
+
const now = this.deps.now().getTime();
|
|
877
|
+
const overdue = deployment.firstWeek.filter((t) => t.status === 'scheduled' && Date.parse(t.dueAt) <= now).length;
|
|
878
|
+
const sent = deployment.firstWeek.filter((t) => t.status === 'sent').length;
|
|
879
|
+
const later = deployment.firstWeek.length - sent - failed - overdue;
|
|
880
|
+
if (failed > 0)
|
|
881
|
+
return { status: 'failed', items, error: `${failed} 件没交出去`, message: '有任务没交给团队,重新部署会再试' };
|
|
882
|
+
if (overdue > 0) {
|
|
883
|
+
return { status: 'pending', reason: BUNDLE_CONSTANTS.PENDING_REASONS.BACKEND_NOT_RUNNING, items, message: 'Crewly 启动后马上把今天的任务交给团队' };
|
|
884
|
+
}
|
|
885
|
+
return { status: 'done', items, message: `已交给团队 ${sent} 件${later > 0 ? `,另外 ${later} 件按天排好` : ''}` };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
//# sourceMappingURL=bundle-apply.service.js.map
|