Files
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

296 lines
8.2 KiB
Go

package projectworkflow
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"time"
"chat-agent-system/internal/auth"
)
// Service 项目多工作流与血缘领域服务
type Service struct {
store Store
catalog Catalog
}
// NewService 构造项目工作流服务
func NewService(store Store, catalog Catalog) *Service {
if catalog == nil {
catalog = NewDefaultCatalog()
}
return &Service{
store: store,
catalog: catalog,
}
}
// Catalog 获取静态工作流规范目录
func (s *Service) Catalog() Catalog {
return s.catalog
}
// Store 获取持久化存储接口
func (s *Service) Store() Store {
return s.store
}
// CreateProject 创建新项目,严格从认证令牌注入权威创建人,并自动初始化挂载工作流
func (s *Service) CreateProject(ctx context.Context, user *auth.UserAccount, in CreateProjectInput) (*Project, error) {
if user == nil || user.ID <= 0 {
return nil, ErrUnauthenticated
}
name := strings.TrimSpace(in.Name)
if name == "" {
return nil, fmt.Errorf("%w: 项目名称为必填项", ErrInvalidInput)
}
code := strings.TrimSpace(in.Code)
if code == "" {
code = generateProjectCode(name)
}
initialWorkflow := strings.TrimSpace(in.InitialWorkflowKey)
if initialWorkflow != "" {
if _, ok := s.catalog.Get(initialWorkflow); !ok {
return nil, fmt.Errorf("%w: 初始工作流 %s 不存在", ErrWorkflowNotFound, initialWorkflow)
}
}
p := &Project{
Code: code,
Name: name,
CustomerName: in.CustomerName,
DeviceType: in.DeviceType,
IntendedUse: in.IntendedUse,
TargetMarket: in.TargetMarket,
CurrentStage: "V0.1_triage",
Environment: "dev",
Status: "active",
CreatorUserID: user.ID,
LifecycleStatus: "planning",
ActiveWorkflowKey: "",
WorkflowSelectionVersion: 1,
WorkflowPlanRevision: 0,
}
if initialWorkflow != "" {
p.LifecycleStatus = "active"
p.ActiveWorkflowKey = initialWorkflow
}
created, err := s.store.CreateProject(ctx, p)
if err != nil {
return nil, err
}
// 自动挂载目录中的工作流实例
var allKeys []string
for _, def := range s.catalog.List() {
allKeys = append(allKeys, def.Key)
}
if err := s.store.EnsureProjectWorkflows(ctx, created.ID, allKeys); err != nil {
return nil, fmt.Errorf("initialize project workflows failed: %w", err)
}
return created, nil
}
// GetProject 根据项目 ID 获取项目详情
func (s *Service) GetProject(ctx context.Context, projectID int64, userID int64) (*Project, error) {
if projectID <= 0 {
return nil, ErrProjectNotFound
}
p, err := s.store.GetProjectByID(ctx, projectID)
if err != nil {
return nil, err
}
return p, nil
}
// GetProjectByCode 根据项目唯一编码获取项目详情
func (s *Service) GetProjectByCode(ctx context.Context, code string) (*Project, error) {
return s.store.GetProjectByCode(ctx, code)
}
// ListProjects 查询指定用户可见的项目列表
func (s *Service) ListProjects(ctx context.Context, userID int64) ([]*Project, error) {
return s.store.ListProjectsForUser(ctx, userID)
}
// ListAllProjects 查询全部项目,仅供已完成系统管理员鉴权的调用方使用。
func (s *Service) ListAllProjects(ctx context.Context) ([]*Project, error) {
return s.store.ListAllProjects(ctx)
}
// IsProjectCreator 判断用户是否为指定项目的权威创建人
func (s *Service) IsProjectCreator(ctx context.Context, projectID int64, userID int64) (bool, error) {
p, err := s.store.GetProjectByID(ctx, projectID)
if err != nil {
return false, err
}
return p.CreatorUserID == userID, nil
}
// ListProjectWorkflows 查询项目已挂载的工作流清单
func (s *Service) ListProjectWorkflows(ctx context.Context, projectID int64) ([]*ProjectWorkflow, error) {
// 确保默认工作流已初始化
var allKeys []string
for _, def := range s.catalog.List() {
allKeys = append(allKeys, def.Key)
}
_ = s.store.EnsureProjectWorkflows(ctx, projectID, allKeys)
return s.store.ListProjectWorkflows(ctx, projectID)
}
// ActivateWorkflow 原子切换项目当前激活的工作流焦点(仅限创建人)
func (s *Service) ActivateWorkflow(
ctx context.Context,
projectID int64,
userID int64,
in ActivateWorkflowInput,
) (*ActivateWorkflowResult, error) {
if userID <= 0 {
return nil, ErrUnauthenticated
}
targetKey := strings.TrimSpace(in.WorkflowKey)
if targetKey == "" {
return nil, fmt.Errorf("%w: target workflow_key cannot be empty", ErrInvalidInput)
}
def, ok := s.catalog.Get(targetKey)
if !ok || !def.Enabled {
return nil, fmt.Errorf("%w: workflow '%s' is not supported or disabled", ErrWorkflowNotFound, targetKey)
}
updatedProject, audit, err := s.store.UpdateActiveWorkflow(
ctx,
projectID,
targetKey,
in.ExpectedVersion,
userID,
in.Reason,
)
if err != nil {
return nil, err
}
now := time.Now()
if audit != nil {
now = audit.CreatedAt
} else if updatedProject.ActiveWorkflowChangedAt != nil {
now = *updatedProject.ActiveWorkflowChangedAt
}
return &ActivateWorkflowResult{
ProjectID: updatedProject.ID,
ActiveWorkflowKey: updatedProject.ActiveWorkflowKey,
Version: updatedProject.WorkflowSelectionVersion,
ChangedAt: now,
ChangedBy: userID,
}, nil
}
// CreateWorkflowRun 创建工作流运行实例
func (s *Service) CreateWorkflowRun(ctx context.Context, run *WorkflowRun) (*WorkflowRun, error) {
if run.RunID == "" {
run.RunID = generateRunID(run.WorkflowKey)
}
if run.Status == "" {
run.Status = "running"
}
return s.store.CreateWorkflowRun(ctx, run)
}
// UpdateWorkflowRun 结束/更新工作流运行实例
func (s *Service) UpdateWorkflowRun(
ctx context.Context,
runID string,
status string,
outputDocID string,
summary string,
errMsg string,
) error {
return s.store.UpdateWorkflowRun(ctx, runID, status, outputDocID, summary, errMsg)
}
// GetWorkflowRun 获取运行记录详情
func (s *Service) GetWorkflowRun(ctx context.Context, runID string) (*WorkflowRun, error) {
return s.store.GetWorkflowRun(ctx, runID)
}
// ListWorkflowRuns 列出项目的运行历史
func (s *Service) ListWorkflowRuns(ctx context.Context, projectID int64, workflowKey string, limit int) ([]*WorkflowRun, error) {
return s.store.ListWorkflowRuns(ctx, projectID, workflowKey, limit)
}
// RecordLineage 记录文档版本血缘
func (s *Service) RecordLineage(ctx context.Context, lineage *DocumentLineage) error {
return s.store.RecordLineage(ctx, lineage)
}
// GetLineageForDownstream 查询下游文档所依赖的上游版本信息
func (s *Service) GetLineageForDownstream(ctx context.Context, docID string, version int) ([]*DocumentLineage, error) {
return s.store.GetLineageForDownstream(ctx, docID, version)
}
// CheckAndPropagateStale 当上游文档更新且内容哈希变化时,通知并将关联下游文档标记为 stale
func (s *Service) CheckAndPropagateStale(
ctx context.Context,
upstreamDocID string,
newSHA256 string,
reason string,
) ([]*DocumentLineage, error) {
lineages, err := s.store.GetLineagesForUpstream(ctx, upstreamDocID)
if err != nil {
return nil, err
}
needsStale := false
for _, l := range lineages {
if l.UpstreamSHA256 != "" && l.UpstreamSHA256 != newSHA256 && !l.IsStale {
needsStale = true
break
}
}
if !needsStale {
return nil, nil
}
if reason == "" {
reason = fmt.Sprintf("上游文档 %s 生成了新版本,内容已发生实质变化", upstreamDocID)
}
return s.store.MarkDownstreamStale(ctx, upstreamDocID, reason)
}
// 辅助方法:生成项目短代码(按 rune 截断,避免切断 UTF-8 多字节字符导致 PG 插入失败)
func generateProjectCode(name string) string {
b := make([]byte, 4)
_, _ = rand.Read(b)
hash := hex.EncodeToString(b)
cleanName := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(name), " ", "-"))
runes := []rune(cleanName)
if len(runes) > 12 {
runes = runes[:12]
cleanName = string(runes)
}
if cleanName == "" {
cleanName = "proj"
}
return fmt.Sprintf("%s-%s", cleanName, hash)
}
// 辅助方法:生成运行实例 ID
func generateRunID(workflowKey string) string {
b := make([]byte, 6)
_, _ = rand.Read(b)
return fmt.Sprintf("run_%s_%s", workflowKey, hex.EncodeToString(b))
}