Files
Agent-System/internal/projectinstance/validator.go
T
weichenandCursor f07178fa73 feat(service): enforce three-workflow governance and instance lifecycle
Wire catalog policy, v2 manifests, stage access, activation metadata fixes, and coordinator instance advance around frozen workflow snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

280 lines
8.0 KiB
Go

package projectinstance
import (
"fmt"
"chat-agent-system/internal/workflowdef"
)
func validatePlan(plan WorkflowPlan, version *workflowdef.Version) workflowdef.ValidationResult {
issues := make([]workflowdef.ValidationIssue, 0)
if version == nil {
return workflowdef.ValidationResult{
Valid: false,
Issues: []workflowdef.ValidationIssue{{
Code: "WORKFLOW_VERSION_NOT_FOUND", Message: "工作流版本不存在或未发布",
}},
}
}
if version.Status != workflowdef.StatusPublished {
issues = append(issues, workflowdef.ValidationIssue{
Code: "WORKFLOW_NOT_PUBLISHED", Message: "只能绑定已发布的工作流版本",
})
}
if !workflowdef.IsOfficialWorkflowKey(plan.WorkflowDefinitionKey) {
issues = append(issues, workflowdef.ValidationIssue{
Code: "WORKFLOW_NOT_SELECTABLE", Field: "workflow_definition_key",
Message: "仅允许选择正式三工作流",
})
}
if plan.WorkflowDefinitionKey != version.WorkflowKey {
issues = append(issues, workflowdef.ValidationIssue{
Code: "WORKFLOW_KEY_MISMATCH", Field: "workflow_definition_key",
Message: fmt.Sprintf("规划 key %s 与版本 %s 不一致", plan.WorkflowDefinitionKey, version.WorkflowKey),
})
}
if plan.WorkflowVersion != version.Version {
issues = append(issues, workflowdef.ValidationIssue{
Code: "WORKFLOW_VERSION_MISMATCH", Field: "workflow_version",
Message: fmt.Sprintf("规划版本 %d 与已发布版本 %d 不一致", plan.WorkflowVersion, version.Version),
})
}
stageDefMap := make(map[string]workflowdef.StageDef, len(version.Definition.Stages))
for _, st := range version.Definition.Stages {
stageDefMap[st.Key] = st
}
seenStage := map[string]bool{}
for _, st := range plan.Stages {
if seenStage[st.StageKey] {
issues = append(issues, workflowdef.ValidationIssue{
Code: "DUPLICATE_STAGE", Field: st.StageKey, Message: "阶段 key 重复",
})
continue
}
seenStage[st.StageKey] = true
def, ok := stageDefMap[st.StageKey]
if !ok {
issues = append(issues, workflowdef.ValidationIssue{
Code: "UNKNOWN_STAGE", Field: st.StageKey, Message: "阶段不在工作流定义中",
})
continue
}
if !def.Optional && !st.Enabled {
issues = append(issues, workflowdef.ValidationIssue{
Code: "REQUIRED_STAGE_DISABLED", Field: st.StageKey, Message: "必需阶段不能关闭",
})
}
taskDefMap := make(map[string]workflowdef.DocTaskDef, len(def.DocTasks))
for _, dt := range def.DocTasks {
taskDefMap[dt.Key] = dt
}
seenTask := map[string]bool{}
for _, dt := range st.DocTasks {
if seenTask[dt.TaskKey] {
issues = append(issues, workflowdef.ValidationIssue{
Code: "DUPLICATE_DOC_TASK", Field: dt.TaskKey, Message: "文档任务 key 重复",
})
continue
}
seenTask[dt.TaskKey] = true
td, ok := taskDefMap[dt.TaskKey]
if !ok {
issues = append(issues, workflowdef.ValidationIssue{
Code: "UNKNOWN_DOC_TASK", Field: dt.TaskKey, Message: "文档任务不在阶段定义中",
})
continue
}
if !dt.Enabled && !td.IsCreatorConfigurable() {
issues = append(issues, workflowdef.ValidationIssue{
Code: "REQUIRED_DOC_DISABLED", Field: dt.TaskKey, Message: "该文档任务不可由创建者关闭",
})
}
if dt.Enabled {
issues = append(issues, validateDuties(dt.Duties, dt.TaskKey)...)
}
}
}
enabledStages := 0
for _, st := range plan.Stages {
if st.Enabled {
enabledStages++
}
}
if enabledStages == 0 {
issues = append(issues, workflowdef.ValidationIssue{
Code: "NO_ENABLED_STAGE", Message: "至少启用一个阶段",
})
}
return workflowdef.ValidationResult{Valid: len(issues) == 0, Issues: issues}
}
func validateActivationPlan(plan WorkflowPlan, version *workflowdef.Version) workflowdef.ValidationResult {
base := validatePlan(plan, version)
if !base.Valid {
return base
}
if countEnabledDocTasks(plan) == 0 {
base.Valid = false
base.Issues = append(base.Issues, workflowdef.ValidationIssue{
Code: "EMPTY_WORKFLOW", Message: "至少启用一个文档任务才能激活工作流",
})
}
return base
}
func countEnabledDocTasks(plan WorkflowPlan) int {
n := 0
for _, st := range plan.Stages {
if !st.Enabled {
continue
}
for _, dt := range st.DocTasks {
if dt.Enabled {
n++
}
}
}
return n
}
func validateDuties(d DocDuty, taskKey string) []workflowdef.ValidationIssue {
issues := make([]workflowdef.ValidationIssue, 0)
if d.AuthorUserID <= 0 {
issues = append(issues, workflowdef.ValidationIssue{
Code: "DUTY_AUTHOR_REQUIRED", Field: taskKey + ".author",
Message: "启用的文档任务必须指定作者",
})
}
if d.ReviewerUserID <= 0 {
issues = append(issues, workflowdef.ValidationIssue{
Code: "DUTY_REVIEWER_REQUIRED", Field: taskKey + ".reviewer",
Message: "启用的文档任务必须指定审核人",
})
}
if d.ApproverUserID <= 0 {
issues = append(issues, workflowdef.ValidationIssue{
Code: "DUTY_APPROVER_REQUIRED", Field: taskKey + ".approver",
Message: "启用的文档任务必须指定批准人",
})
}
ids := []int64{d.AuthorUserID, d.ReviewerUserID, d.ApproverUserID}
for i := 0; i < len(ids); i++ {
if ids[i] <= 0 {
continue
}
for j := i + 1; j < len(ids); j++ {
if ids[j] > 0 && ids[i] == ids[j] {
issues = append(issues, workflowdef.ValidationIssue{
Code: "SEPARATION_OF_DUTY_VIOLATION", Field: taskKey,
Message: "作者、审核人、批准人必须分离",
})
return issues
}
}
}
return issues
}
func buildDefaultPlan(version *workflowdef.Version) WorkflowPlan {
plan := WorkflowPlan{
WorkflowDefinitionKey: version.WorkflowKey,
WorkflowVersion: version.Version,
WorkflowVersionID: version.ID,
Stages: make([]StagePlan, 0, len(version.Definition.Stages)),
}
for _, st := range version.Definition.Stages {
sp := StagePlan{
StageKey: st.Key,
StageName: st.Name,
Enabled: !st.Optional,
DocTasks: make([]DocTaskPlan, 0, len(st.DocTasks)),
}
for _, dt := range st.DocTasks {
sp.DocTasks = append(sp.DocTasks, DocTaskPlan{
TaskKey: dt.Key,
TaskTitle: dt.Title,
Enabled: dt.DefaultEnabled,
})
}
plan.Stages = append(plan.Stages, sp)
}
return plan
}
// applyDefinitionLabels 使用已发布定义覆盖展示名称,避免客户端把自定义文本写入冻结快照。
func applyDefinitionLabels(plan *WorkflowPlan, version *workflowdef.Version) {
if plan == nil || version == nil {
return
}
stageDefs := make(map[string]workflowdef.StageDef, len(version.Definition.Stages))
for _, stage := range version.Definition.Stages {
stageDefs[stage.Key] = stage
}
for stageIdx := range plan.Stages {
stage := &plan.Stages[stageIdx]
stageDef, ok := stageDefs[stage.StageKey]
if !ok {
continue
}
stage.StageName = stageDef.Name
taskDefs := make(map[string]workflowdef.DocTaskDef, len(stageDef.DocTasks))
for _, task := range stageDef.DocTasks {
taskDefs[task.Key] = task
}
for taskIdx := range stage.DocTasks {
task := &stage.DocTasks[taskIdx]
if taskDef, exists := taskDefs[task.TaskKey]; exists {
task.TaskTitle = taskDef.Title
}
}
}
}
func previewSummary(plan WorkflowPlan, version *workflowdef.Version) PreviewSummary {
sum := PreviewSummary{
WorkflowVersion: plan.WorkflowVersion,
}
if version != nil {
sum.WorkflowName = version.WorkflowName
if sum.WorkflowName == "" {
sum.WorkflowName = version.WorkflowKey
}
}
for _, st := range plan.Stages {
if !st.Enabled {
continue
}
sum.EnabledStages++
if sum.FirstStageKey == "" {
sum.FirstStageKey = st.StageKey
if version != nil {
for _, sd := range version.Definition.Stages {
if sd.Key == st.StageKey {
sum.FirstStageName = sd.Name
break
}
}
}
}
for _, dt := range st.DocTasks {
if !dt.Enabled {
continue
}
sum.EnabledDocTasks++
if dt.Duties.AuthorUserID > 0 {
sum.AssignedDuties++
}
if dt.Duties.ReviewerUserID > 0 {
sum.AssignedDuties++
}
if dt.Duties.ApproverUserID > 0 {
sum.AssignedDuties++
}
}
}
return sum
}