Unify auth middleware and remove X-User-ID trust, add project_members authorization, sandbox workspaces via resolver, and admin-only reconcile by run_id. Introduce goose migrations with startup schema gate, fail closed when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in app Dockerfile, and remove frontend pseudo-login fallback.
273 lines
8.6 KiB
Go
273 lines
8.6 KiB
Go
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 {
|
|
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req projectworkflow.CreateProjectInput
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
proj, err := s.pwSvc.CreateProject(r.Context(), user, req)
|
|
if err != nil {
|
|
if errors.Is(err, projectworkflow.ErrInvalidInput) {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"`+err.Error()+`"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
|
|
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"`+err.Error()+`"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
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
|
|
}
|
|
|
|
list, err := s.pwSvc.ListProjects(r.Context(), user.ID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
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 {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
|
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) {
|
|
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在或无权限访问"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
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 {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, ok := s.requireProjectAccess(w, r, id); !ok {
|
|
return
|
|
}
|
|
|
|
wfs, err := s.pwSvc.ListProjectWorkflows(r.Context(), id)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if wfs == nil {
|
|
wfs = make([]*projectworkflow.ProjectWorkflow, 0)
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, wfs)
|
|
}
|
|
|
|
// handleActivateWorkflow 切换项目当前激活的工作流焦点(创建人权限受控)
|
|
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 {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
user, err := s.authenticateUser(r)
|
|
if err != nil || user == nil {
|
|
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req projectworkflow.ActivateWorkflowInput
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的请求体"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
res, err := s.pwSvc.ActivateWorkflow(r.Context(), id, user.ID, req)
|
|
if err != nil {
|
|
if errors.Is(err, projectworkflow.ErrForbiddenNotCreator) {
|
|
http.Error(w, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人有权切换激活工作流"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
if errors.Is(err, projectworkflow.ErrConcurrentConflict) {
|
|
http.Error(w, `{"error":"ERR_CONCURRENT_SWITCH","message":"工作流选择版本落后或并发冲突,请刷新后重试"}`, http.StatusConflict)
|
|
return
|
|
}
|
|
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
|
|
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"工作流定义不存在或未在项目启用"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
|
|
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
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 {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
wfKey := r.PathValue("workflow_key")
|
|
if wfKey == "" {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
def, ok := s.pwSvc.Catalog().Get(wfKey)
|
|
if !ok {
|
|
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
user, ok := s.requireProjectAccess(w, r, id)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req TaskRunRequestDTO
|
|
if r.Body != nil {
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
}
|
|
req.ProjectID = id
|
|
req.WorkflowKey = wfKey
|
|
req.UserID = user.ID
|
|
if req.TaskKey == "" {
|
|
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 {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
wfKey := r.PathValue("workflow_key")
|
|
runs, err := s.pwSvc.ListWorkflowRuns(r.Context(), id, wfKey, 20)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if runs == nil {
|
|
runs = make([]*projectworkflow.WorkflowRun, 0)
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, runs)
|
|
}
|
|
|
|
// handleGetProjectLineage 查询项目文档血缘关系及失效状态
|
|
func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request) {
|
|
docID := strings.TrimSpace(r.URL.Query().Get("doc_id"))
|
|
if docID == "" {
|
|
idStr := r.PathValue("id")
|
|
id, _ := strconv.ParseInt(idStr, 10, 64)
|
|
if id > 0 {
|
|
docID = fmt.Sprintf("doc_proj%d_test_manager", id)
|
|
}
|
|
}
|
|
if docID == "" {
|
|
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"doc_id is required"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
lineages, err := s.pwSvc.GetLineageForDownstream(r.Context(), docID, 0)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if len(lineages) == 0 {
|
|
idStr := r.PathValue("id")
|
|
if id, _ := strconv.ParseInt(idStr, 10, 64); id > 0 {
|
|
hyphenDocID := fmt.Sprintf("doc-proj%d-test_manager", id)
|
|
if hyphenDocID != docID {
|
|
if fb, fbErr := s.pwSvc.GetLineageForDownstream(r.Context(), hyphenDocID, 0); fbErr == nil && len(fb) > 0 {
|
|
lineages = fb
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if lineages == nil {
|
|
lineages = make([]*projectworkflow.DocumentLineage, 0)
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, lineages)
|
|
}
|
|
|