Files
weichen f3f23a3018 feat(errcode): 用稳定错误码记录 HTTP 与异步失败
失败响应此前不写日志,request_id 无法对照原因。注册表不覆盖处理器已经给出的 HTTP 状态。
2026-09-28 02:27:16 +00:00

325 lines
10 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 {
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))
}