package server import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "chat-agent-system/internal/projectworkflow" ) // handleCreateProject 服务端权威创建项目接口(严格从 Token 获取创建人) func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) { user, err := s.authenticateUser(r) if err != nil || user == nil { writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`) return } if ok, permErr := s.userHasSystemPermission(r.Context(), user, "project:create"); permErr != nil { writeDomainError(w, r, http.StatusInternalServerError, permErr) return } else if !ok { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权创建项目", }) return } var req projectworkflow.CreateProjectInput if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`) return } proj, err := s.pwSvc().CreateProject(r.Context(), user, req) if err != nil { if errors.Is(err, projectworkflow.ErrInvalidInput) { writeDomainError(w, r, http.StatusBadRequest, err) return } if errors.Is(err, projectworkflow.ErrDuplicateProjectName) { writeJSONResponse(w, http.StatusConflict, map[string]string{ "error_code": "ERR_DUPLICATE_PROJECT_NAME", "message": "项目名已被占用,请更换名称后重试", }) return } if errors.Is(err, projectworkflow.ErrWorkflowNotFound) { writeDomainError(w, r, http.StatusBadRequest, err) return } writeDomainError(w, r, http.StatusInternalServerError, err) return } if proj != nil && s.authorizer() != nil { _ = s.authorizer().AddOwnerOnCreate(r.Context(), proj.ID, user.ID) } writeJSONResponse(w, http.StatusCreated, proj) } // handleListProjects 查询项目清单;兼容 session_id 检索旧行为 func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) { if sessionID := r.URL.Query().Get("session_id"); sessionID != "" { s.handleGetProject(w, r) return } user, ok := s.requirePrincipal(w, r) if !ok { return } isAdmin, err := s.authzSvc().IsSystemAdmin(r.Context(), user.ID, user.RoleID) if err != nil { writeJSONResponse(w, http.StatusInternalServerError, map[string]string{ "error_code": "ERR_AUTHZ", "message": "权限判定失败", }) return } var list []*projectworkflow.Project if isAdmin { list, err = s.pwSvc().ListAllProjects(r.Context()) } else { list, err = s.pwSvc().ListProjects(r.Context(), user.ID) } if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } if list == nil { list = make([]*projectworkflow.Project, 0) } writeJSONResponse(w, http.StatusOK, list) } // handleGetProjectByID 获取指定项目的详细信息 func (s *Server) handleGetProjectByID(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`) return } if _, ok := s.requireProjectAccess(w, r, id); !ok { return } proj, err := s.pwSvc().GetProject(r.Context(), id, 0) if err != nil { if errors.Is(err, projectworkflow.ErrProjectNotFound) { writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在或无权限访问"}`) return } writeDomainError(w, r, http.StatusInternalServerError, err) return } writeJSONResponse(w, http.StatusOK, proj) } // handleListProjectWorkflows 获取项目挂载的工作流清单 func (s *Server) handleListProjectWorkflows(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`) return } if _, ok := s.requireProjectAccess(w, r, id); !ok { return } wfs, err := s.pwSvc().ListProjectWorkflows(r.Context(), id) if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } if wfs == nil { wfs = make([]*projectworkflow.ProjectWorkflow, 0) } writeJSONResponse(w, http.StatusOK, wfs) } // handleActivateWorkflow 旧式工作流切换 API(已退役)。 func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`) return } user, err := s.authenticateUser(r) if err != nil || user == nil { writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`) return } if s.instSvc() != nil && s.instSvc().Available() { writeSafeError(w, r, http.StatusGone, `{"error":"WORKFLOW_SWITCH_API_RETIRED","message":"请通过项目设置完成工作流规划与激活"}`) return } if s.blockLegacyWorkflowSwitch(w, r, id) { return } var req projectworkflow.ActivateWorkflowInput if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的请求体"}`) return } res, err := s.pwSvc().ActivateWorkflow(r.Context(), id, user.ID, req) if err != nil { if errors.Is(err, projectworkflow.ErrForbiddenNotCreator) { writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人有权切换激活工作流"}`) return } if errors.Is(err, projectworkflow.ErrConcurrentConflict) { writeSafeError(w, r, http.StatusConflict, `{"error":"ERR_CONCURRENT_SWITCH","message":"工作流选择版本落后或并发冲突,请刷新后重试"}`) return } if errors.Is(err, projectworkflow.ErrWorkflowNotFound) { writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"工作流定义不存在或未在项目启用"}`) return } if errors.Is(err, projectworkflow.ErrProjectNotFound) { writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`) return } writeDomainError(w, r, http.StatusInternalServerError, err) return } writeJSONResponse(w, http.StatusOK, res) } // handleListWorkflowDefinitions 获取全局工作流规范目录 func (s *Server) handleListWorkflowDefinitions(w http.ResponseWriter, r *http.Request) { defs := s.pwSvc().Catalog().List() writeJSONResponse(w, http.StatusOK, defs) } // handleRunProjectWorkflow 针对特定项目与工作流触发运行并落盘运行记录 func (s *Server) handleRunProjectWorkflow(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`) return } wfKey := r.PathValue("workflow_key") if wfKey == "" { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`) return } def, ok := s.pwSvc().Catalog().Get(wfKey) if !ok { writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`) return } user, ok := s.requireProjectAccess(w, r, id) if !ok { return } var req TaskRunRequestDTO if r.Body != nil { _ = json.NewDecoder(r.Body).Decode(&req) } if req.TaskKey != "" && req.TaskKey != def.TaskKey && s.failClosedAuthz() { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"客户端不可覆盖 task_key"}`) return } if !s.allowWorkflowDocTaskAccess(r.Context(), id, user.ID, def.TaskKey) { s.writeWorkflowAccessDenied(w) return } req.ProjectID = id req.WorkflowKey = wfKey req.UserID = user.ID req.TaskKey = def.TaskKey bodyBytes, _ := json.Marshal(req) r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) s.handleRunTask(w, r) } // handleListProjectWorkflowRuns 查询项目某工作流的历史运行记录 func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`) return } if _, ok := s.requireProjectAccess(w, r, id); !ok { return } wfKey := r.PathValue("workflow_key") runs, err := s.pwSvc().ListWorkflowRuns(r.Context(), id, wfKey, 20) if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } if runs == nil { runs = make([]*projectworkflow.WorkflowRun, 0) } writeJSONResponse(w, http.StatusOK, runs) } // handleGetProjectLineage 查询项目文档血缘关系及失效状态。 // doc_id 必须属于 URL 中的项目;不可见或不属于该项目时返回空列表,不暴露对象是否存在。 func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") projectID, _ := strconv.ParseInt(idStr, 10, 64) if projectID <= 0 { writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`) return } user, ok := s.requireProjectAccess(w, r, projectID) if !ok { return } docID := strings.TrimSpace(r.URL.Query().Get("doc_id")) if docID == "" { docID = fmt.Sprintf("doc_proj%d_test_manager", projectID) } if !s.lineageDocVisible(r.Context(), projectID, user.ID, docID) { writeEmptyLineage(w) return } lineages, err := s.pwSvc().GetLineageForProjectDownstream(r.Context(), projectID, docID, 0) if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } if len(lineages) == 0 { if alt, aliasOK := lineageTestManagerAlias(projectID, docID); aliasOK && s.lineageDocVisible(r.Context(), projectID, user.ID, alt) { if fb, fbErr := s.pwSvc().GetLineageForProjectDownstream(r.Context(), projectID, alt, 0); fbErr == nil && len(fb) > 0 { lineages = fb } } } visible := func(id string) bool { return s.lineageDocVisible(r.Context(), projectID, user.ID, id) } writeJSONResponse(w, http.StatusOK, filterLineageEdges(lineages, projectID, visible)) }