package projectinstance import ( "context" "crypto/rand" "encoding/hex" "fmt" "time" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/workflowdef" ) // VersionResolver 解析已发布工作流版本。 type VersionResolver interface { GetDefinitionByKey(ctx context.Context, key string) (*workflowdef.Definition, error) GetVersion(ctx context.Context, workflowID int64, version int) (*workflowdef.Version, error) ListDefinitions(ctx context.Context) ([]workflowdef.Definition, error) } // Service 项目工作流实例与激活领域服务。 type Service struct { store Store wfStore VersionResolver projects projectworkflow.Store docs DocumentReadinessChecker } func NewService(store Store, wfStore VersionResolver, projects projectworkflow.Store, docs DocumentReadinessChecker) *Service { return &Service{store: store, wfStore: wfStore, projects: projects, docs: docs} } func (s *Service) Available() bool { return s != nil && s.store != nil && s.wfStore != nil } func (s *Service) HasLiveInstance(ctx context.Context, projectID int64) (bool, error) { if !s.Available() { return false, nil } return s.store.HasLiveInstance(ctx, projectID) } func (s *Service) GetSettings(ctx context.Context, projectID int64, userID int64, isCreator bool) (*ProjectSettings, error) { if projectID <= 0 { return nil, ErrProjectNotFound } lifecycle, activeKey, selVer, planRev, err := s.store.GetProjectMeta(ctx, projectID) if err != nil { return nil, err } settings := &ProjectSettings{ ProjectID: projectID, LifecycleStatus: lifecycle, ActiveWorkflowKey: activeKey, WorkflowSelectionVersion: selVer, WorkflowPlanRevision: planRev, CanEditPlan: isCreator && lifecycle == LifecyclePlanning, CanActivate: isCreator && lifecycle == LifecyclePlanning, } planInst, err := s.store.GetPlanningInstance(ctx, projectID) if err != nil { return nil, err } if planInst != nil { settings.PlanningInstance = planInst settings.Plan = &planInst.ConfigSnapshot } liveInst, err := s.store.GetLiveInstance(ctx, projectID) if err != nil { return nil, err } if liveInst != nil { settings.LiveInstance = liveInst settings.CanEditPlan = false settings.CanActivate = false if detail, detailErr := s.store.GetInstanceByID(ctx, liveInst.ID); detailErr == nil && detail != nil { settings.ActiveStageKey = detail.ActiveStageKey if detail.ActiveStageKey != "" && s.docs != nil { if version, verErr := s.resolvePublishedVersion(ctx, liveInst.ConfigSnapshot); verErr == nil { if stagePlan := findStagePlan(liveInst.ConfigSnapshot, detail.ActiveStageKey); stagePlan != nil { docStates, _ := s.docs.TaskApprovalStates(ctx, projectID) ready, _, _ := evaluateStageReadiness(*stagePlan, findStageDef(version.Definition, detail.ActiveStageKey), docStates) settings.StageAdvanceReady = ready } } } } } if s.wfStore != nil { defs, listErr := s.wfStore.ListDefinitions(ctx) if listErr == nil { for _, d := range defs { if d.LatestPublishedVersion <= 0 || !workflowdef.IsOfficialWorkflowKey(d.Key) { continue } ver, verErr := s.wfStore.GetVersion(ctx, d.ID, d.LatestPublishedVersion) if verErr != nil || ver == nil { continue } name := d.Name if name == "" || name == d.Key { name = workflowdef.OfficialWorkflowName(d.Key) } settings.PublishedWorkflows = append(settings.PublishedWorkflows, PublishedOption{ DefinitionKey: d.Key, DefinitionName: name, LatestVersion: d.LatestPublishedVersion, WorkflowVersion: ver.ID, ProductDomain: d.ProductDomain, }) } } } _ = userID return settings, nil } func (s *Service) SaveWorkflowPlan(ctx context.Context, projectID int64, userID int64, isCreator bool, in SavePlanInput) (*ProjectSettings, error) { if !isCreator { return nil, ErrForbiddenNotCreator } lifecycle, _, selVer, planRev, err := s.store.GetProjectMeta(ctx, projectID) if err != nil { return nil, err } if lifecycle != LifecyclePlanning { return nil, ErrPlanFrozen } live, err := s.store.HasLiveInstance(ctx, projectID) if err != nil { return nil, err } if live { return nil, ErrWorkflowAlreadyActive } if in.ExpectedSelectionVersion > 0 && in.ExpectedSelectionVersion != selVer { return nil, ErrConcurrentConflict } if in.ExpectedPlanRevision != planRev { return nil, ErrPlanRevisionConflict } version, err := s.resolvePublishedVersion(ctx, in.Plan) if err != nil { return nil, err } validation := validatePlan(in.Plan, version) if !validation.Valid { return nil, newValidationError(validation) } in.Plan.WorkflowVersionID = version.ID applyDefinitionLabels(&in.Plan, version) planHash, err := computePlanHash(in.Plan) if err != nil { return nil, err } inst := &WorkflowInstance{ ProjectID: projectID, WorkflowVersionID: version.ID, Status: InstancePlanning, ConfigRevision: planRev + 1, PlanHash: planHash, ConfigSnapshot: in.Plan, CreatedBy: &userID, } if _, err := s.store.UpsertPlanningInstance(ctx, inst); err != nil { return nil, err } if _, err := s.store.BumpPlanRevision(ctx, projectID, planRev); err != nil { return nil, err } return s.GetSettings(ctx, projectID, userID, isCreator) } func (s *Service) ActivationPreview(ctx context.Context, projectID int64, userID int64, isCreator bool) (*ActivationPreview, error) { if !isCreator { return nil, ErrForbiddenNotCreator } lifecycle, _, selVer, planRev, err := s.store.GetProjectMeta(ctx, projectID) if err != nil { return nil, err } if lifecycle != LifecyclePlanning { return nil, ErrPlanFrozen } planInst, err := s.store.GetPlanningInstance(ctx, projectID) if err != nil { return nil, err } if planInst == nil || planInst.PlanHash == "" { return nil, fmt.Errorf("%w: no saved plan", ErrInvalidPlan) } version, err := s.resolvePublishedVersion(ctx, planInst.ConfigSnapshot) if err != nil { return nil, err } validation := validateActivationPlan(planInst.ConfigSnapshot, version) if !validation.Valid { return nil, newValidationError(validation) } token, err := randomToken(24) if err != nil { return nil, err } expiresAt := time.Now().Add(15 * time.Minute) if err := s.store.SaveActivationToken(ctx, token, projectID, planInst.PlanHash, selVer, planInst.ConfigSnapshot, userID, expiresAt); err != nil { return nil, err } return &ActivationPreview{ ConfirmationToken: token, PlanHash: planInst.PlanHash, ExpectedProjectVersion: selVer, ExpectedPlanRevision: planRev, Summary: previewSummary(planInst.ConfigSnapshot, version), Validation: validation, ExpiresAt: expiresAt, }, nil } func (s *Service) Activate(ctx context.Context, projectID int64, userID int64, isCreator bool, in ActivateInput) (*ActivateResult, error) { if !isCreator { return nil, ErrForbiddenNotCreator } lifecycle, _, selVer, planRev, err := s.store.GetProjectMeta(ctx, projectID) if err != nil { return nil, err } if lifecycle != LifecyclePlanning { return nil, ErrPlanFrozen } if in.ExpectedProjectVersion != selVer { return nil, ErrConcurrentConflict } if in.ExpectedPlanRevision != planRev { return nil, ErrPlanRevisionConflict } planInst, err := s.store.GetPlanningInstance(ctx, projectID) if err != nil { return nil, err } if planInst == nil { return nil, fmt.Errorf("%w: no saved plan", ErrInvalidPlan) } consumed, err := s.store.ConsumeActivationToken(ctx, in.ConfirmationToken, projectID, planInst.PlanHash) if err != nil { return nil, err } currentHash, err := computePlanHash(*consumed) if err != nil { return nil, err } if currentHash != planInst.PlanHash { return nil, ErrPlanChangedReconfirm } version, err := s.resolvePublishedVersion(ctx, planInst.ConfigSnapshot) if err != nil { return nil, err } validation := validateActivationPlan(planInst.ConfigSnapshot, version) if !validation.Valid { return nil, ErrValidationFailed } activated, err := s.store.ActivateInstance(ctx, projectID, planInst, version, selVer, userID) if err != nil { return nil, err } return &ActivateResult{ ProjectID: projectID, LifecycleStatus: LifecycleActive, ActiveWorkflowKey: version.WorkflowKey, Instance: *activated, }, nil } // GetLiveInstanceDetail 返回 live 实例详情与已发布版本定义(运行时编排入口)。 func (s *Service) GetLiveInstanceDetail(ctx context.Context, projectID int64) (*InstanceDetail, *workflowdef.Version, error) { if s == nil || s.store == nil { return nil, nil, ErrNotAvailable } live, err := s.store.GetLiveInstance(ctx, projectID) if err != nil || live == nil { return nil, nil, fmt.Errorf("live instance not found") } detail, err := s.store.GetInstanceByID(ctx, live.ID) if err != nil { return nil, nil, err } version, err := s.resolvePublishedVersion(ctx, live.ConfigSnapshot) if err != nil { return nil, nil, err } return detail, version, nil } func (s *Service) BuildDefaultPlan(ctx context.Context, workflowKey string, versionNum int) (*WorkflowPlan, error) { if s.wfStore == nil { return nil, ErrNotAvailable } if !workflowdef.IsOfficialWorkflowKey(workflowKey) { return nil, ErrWorkflowVersionNotFound } def, err := s.wfStore.GetDefinitionByKey(ctx, workflowKey) if err != nil { return nil, ErrWorkflowVersionNotFound } verNum := versionNum if verNum <= 0 { verNum = def.LatestPublishedVersion } version, err := s.wfStore.GetVersion(ctx, def.ID, verNum) if err != nil { return nil, ErrWorkflowVersionNotFound } plan := buildDefaultPlan(version) if plan.WorkflowDefinitionKey == "" { plan.WorkflowDefinitionKey = def.Key } return &plan, nil } func (s *Service) resolvePublishedVersion(ctx context.Context, plan WorkflowPlan) (*workflowdef.Version, error) { if s.wfStore == nil { return nil, ErrNotAvailable } def, err := s.wfStore.GetDefinitionByKey(ctx, plan.WorkflowDefinitionKey) if err != nil { return nil, ErrWorkflowVersionNotFound } verNum := plan.WorkflowVersion if verNum <= 0 { verNum = def.LatestPublishedVersion } version, err := s.wfStore.GetVersion(ctx, def.ID, verNum) if err != nil { return nil, ErrWorkflowVersionNotFound } version.WorkflowKey = def.Key version.WorkflowName = def.Name return version, nil } func randomToken(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }