package projectinstance import ( "context" "errors" "fmt" ) const ( ScopeWorkflowStage = "workflow_stage" ScopePreActivation = "pre_activation" ScopeLegacyUnscoped = "legacy_unscoped" ScopeNonProjectPrivate = "non_project_private" ) var ErrTaskScopeNotResolved = errors.New("无法唯一解析文档任务的工作流阶段作用域") // MessageScope 不可变消息工作流作用域(服务端解析,客户端不可伪造)。 type MessageScope struct { ScopeKind string `json:"scope_kind"` WorkflowInstanceID int64 `json:"workflow_instance_id,omitempty"` StageInstanceID int64 `json:"stage_instance_id,omitempty"` StageKey string `json:"stage_key,omitempty"` } // ApplyScopeToMetadata 将作用域写入 metadata(冗余,便于审计与兼容)。 func ApplyScopeToMetadata(meta map[string]any, scope *MessageScope) { if meta == nil || scope == nil { return } meta["scope_kind"] = scope.ScopeKind if scope.WorkflowInstanceID > 0 { meta["workflow_instance_id"] = scope.WorkflowInstanceID } if scope.StageInstanceID > 0 { meta["stage_instance_id"] = scope.StageInstanceID } if scope.StageKey != "" { meta["stage_key"] = scope.StageKey } } // ScopeFromMetadata 从 metadata 提取作用域(历史兼容)。 func ScopeFromMetadata(metadata map[string]any) MessageScope { out := MessageScope{ScopeKind: ScopeLegacyUnscoped} if metadata == nil { return out } if sk, ok := metadata["scope_kind"].(string); ok && sk != "" { out.ScopeKind = sk } if v, ok := metadata["workflow_instance_id"].(float64); ok && v > 0 { out.WorkflowInstanceID = int64(v) } else if v, ok := metadata["workflow_instance_id"].(int64); ok && v > 0 { out.WorkflowInstanceID = v } if v, ok := metadata["stage_instance_id"].(float64); ok && v > 0 { out.StageInstanceID = int64(v) } else if v, ok := metadata["stage_instance_id"].(int64); ok && v > 0 { out.StageInstanceID = v } out.StageKey = MessageStageKey(metadata) return out } // ResolveMessageScopeForChat 解析普通聊天消息作用域。 func (s *Service) ResolveMessageScopeForChat(ctx context.Context, projectID int64, activeStageKey string) (*MessageScope, error) { if projectID <= 0 { return &MessageScope{ScopeKind: ScopeNonProjectPrivate}, nil } if s == nil || s.store == nil { return &MessageScope{ScopeKind: ScopePreActivation}, nil } live, err := s.store.GetLiveInstance(ctx, projectID) if err != nil || live == nil { return &MessageScope{ScopeKind: ScopePreActivation}, nil } detail, err := s.store.GetInstanceByID(ctx, live.ID) if err != nil || detail == nil { return &MessageScope{ScopeKind: ScopePreActivation}, nil } stageKey := activeStageKey if stageKey == "" { stageKey = detail.ActiveStageKey } if stageKey == "" { return &MessageScope{ ScopeKind: ScopePreActivation, WorkflowInstanceID: live.ID, }, nil } for _, st := range detail.Stages { if st.StageKey == stageKey { return &MessageScope{ ScopeKind: ScopeWorkflowStage, WorkflowInstanceID: live.ID, StageInstanceID: st.ID, StageKey: st.StageKey, }, nil } } return &MessageScope{ ScopeKind: ScopeWorkflowStage, WorkflowInstanceID: live.ID, StageKey: stageKey, }, nil } // ResolveMessageScopeForTask 解析文档任务消息作用域。 // // 经 GetGoverningInstanceForAccess 三态: // - nil(从未激活)→ ScopePreActivation(MessageVisible:仅 session owner) // - live 系(active / awaiting_stage_confirmation / termination_requested)→ 阶段作用域 // - completed / terminated → ErrTaskScopeNotResolved(不得当预激活;access gate 应已先拒) // // 存储错误原样上抛(fail-closed)。禁止仅凭 GetLiveInstance=nil 放行。 func (s *Service) ResolveMessageScopeForTask(ctx context.Context, projectID int64, taskKey string) (*MessageScope, error) { if projectID <= 0 || taskKey == "" { return nil, fmt.Errorf("%w: project_id=%d task_key=%q", ErrTaskScopeNotResolved, projectID, taskKey) } if s == nil || s.store == nil { return nil, ErrTaskScopeNotResolved } gov, err := s.store.GetGoverningInstanceForAccess(ctx, projectID) if err != nil { return nil, err } if gov == nil { return &MessageScope{ScopeKind: ScopePreActivation}, nil } if IsClosedInstanceStatus(gov.Status) { return nil, fmt.Errorf("%w: workflow instance closed (status=%s) for project %d", ErrTaskScopeNotResolved, gov.Status, projectID) } if !IsLiveInstanceStatus(gov.Status) { return nil, fmt.Errorf("%w: unexpected governing status %q for project %d", ErrTaskScopeNotResolved, gov.Status, projectID) } detail, err := s.store.GetInstanceByID(ctx, gov.ID) if err != nil { return nil, err } if detail == nil { return nil, ErrTaskScopeNotResolved } var matches []MessageScope for _, st := range detail.Stages { for _, dt := range st.DocTasks { if dt.TaskKey == taskKey { matches = append(matches, MessageScope{ ScopeKind: ScopeWorkflowStage, WorkflowInstanceID: gov.ID, StageInstanceID: st.ID, StageKey: st.StageKey, }) } } } if len(matches) == 0 { stageKey := TaskKeyToStage(gov.ConfigSnapshot, taskKey) if stageKey == "" { return nil, fmt.Errorf("%w: task_key %q not in instance snapshot", ErrTaskScopeNotResolved, taskKey) } for _, st := range detail.Stages { if st.StageKey == stageKey { return &MessageScope{ ScopeKind: ScopeWorkflowStage, WorkflowInstanceID: gov.ID, StageInstanceID: st.ID, StageKey: st.StageKey, }, nil } } return nil, ErrTaskScopeNotResolved } if len(matches) > 1 { first := matches[0] for _, m := range matches[1:] { if m.StageKey != first.StageKey { return nil, fmt.Errorf("%w: ambiguous task_key %q", ErrTaskScopeNotResolved, taskKey) } } } scope := matches[0] return &scope, nil } // MessageVisible 判断消息对 viewer 是否可见(需会话 owner 与阶段授权上下文)。 func MessageVisible(scope MessageScope, access *StageAccess, sessionOwnerID, viewerUserID int64) bool { if viewerUserID <= 0 { return false } switch scope.ScopeKind { case ScopeNonProjectPrivate, ScopePreActivation: return sessionOwnerID > 0 && viewerUserID == sessionOwnerID case ScopeLegacyUnscoped: if access == nil || !access.Restricted { return sessionOwnerID <= 0 || viewerUserID == sessionOwnerID } return sessionOwnerID > 0 && viewerUserID == sessionOwnerID case ScopeWorkflowStage, "": if access == nil || !access.Restricted { return true } stageKey := scope.StageKey if stageKey == "" { return sessionOwnerID > 0 && viewerUserID == sessionOwnerID } return access.StageKeyAllowed(stageKey) default: return false } } // MessageRecordVisible 从 ChatMessageRecord 字段判定可见性。 func MessageRecordVisible( scopeKind, stageKey string, metadata map[string]any, access *StageAccess, sessionOwnerID, viewerUserID int64, ) bool { scope := MessageScope{ScopeKind: scopeKind, StageKey: stageKey} if scope.ScopeKind == "" { scope = ScopeFromMetadata(metadata) } if scope.StageKey == "" { scope.StageKey = MessageStageKey(metadata) } return MessageVisible(scope, access, sessionOwnerID, viewerUserID) }