package server import ( "encoding/json" "errors" "net/http" "strconv" "strings" "chat-agent-system/internal/authz" "chat-agent-system/internal/freedoc" ) func (s *Server) requireUserID(w http.ResponseWriter, r *http.Request) (int, bool) { user, ok := s.requirePrincipal(w, r) if !ok { return 0, false } return int(user.ID), true } func extractDocIDFromPath(r *http.Request, actionSuffix string) string { path := r.URL.Path if actionSuffix != "" { suffix := "/" + actionSuffix if strings.HasSuffix(path, suffix) { path = strings.TrimSuffix(path, suffix) } } parts := strings.Split(strings.Trim(path, "/"), "/") if len(parts) >= 4 && parts[0] == "api" && parts[1] == "v1" && parts[2] == "freeform-documents" { return parts[3] } return r.PathValue("doc_id") } // POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private) func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } user, ok := s.requirePrincipal(w, r) if !ok { return } var req freedoc.CreateFreeformDocRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_BODY", "message": "请求体格式错误", }) return } if req.ProjectID <= 0 { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_PARAM", "message": "project_id required", }) return } space := strings.TrimSpace(req.Space) if space == "" { space = "my_private" } if space != "my_private" { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_PARAM", "message": "direct create only allows my_private; use share API for project_shared", }) return } req.Space = "my_private" if s.authorizer() == nil { if s.failClosedAuthz() { writeAuthzError(w, authz.ErrForbidden) return } } else if err := s.authorizer().RequireProjectAccessOrAdmin(r.Context(), int64(req.ProjectID), user.ID, s.isSystemAdminUser(r.Context(), user)); err != nil { writeAuthzError(w, err) return } detail, err := s.freeDoc().CreateDocument(r.Context(), int(user.ID), req) if err != nil { if errors.Is(err, freedoc.ErrInvalidParam) { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_PARAM", "message": err.Error(), }) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "error_code": "CREATE_FAILED", "message": err.Error(), }) return } writeJSONResponse(w, http.StatusCreated, map[string]any{ "code": 0, "message": "已注册自由文档", "data": detail, }) } // POST /api/v1/freeform-documents/from-message func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } var req freedoc.CreateFromMessageRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_BODY", "message": "请求体格式错误", }) return } detail, isExisting, err := s.freeDoc().CreateFromMessage(r.Context(), userID, req) if err != nil { if errors.Is(err, freedoc.ErrMessageNotExportable) { writeJSONResponse(w, http.StatusForbidden, map[string]any{ "error_code": "MESSAGE_NOT_EXPORTABLE", "message": "该消息不具备导出或保存资格(纯卡片或未完成)", }) return } if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{ "error_code": "MESSAGE_NOT_FOUND", "message": "指定源消息不存在", }) return } if errors.Is(err, freedoc.ErrConflict) { writeJSONResponse(w, http.StatusConflict, map[string]any{ "error_code": "CONTENT_SHA_MISMATCH", "message": "消息内容哈希校验冲突", }) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "error_code": "SAVE_FAILED", "message": err.Error(), }) return } status := http.StatusCreated if isExisting { status = http.StatusOK } writeJSONResponse(w, status, map[string]any{ "code": 0, "message": "已保存至我的私密", "data": detail, "is_existing": isExisting, }) } // GET /api/v1/freeform-documents func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } space := r.URL.Query().Get("space") if space == "" { space = "my_private" } projectID := int64(0) if pStr := r.URL.Query().Get("project_id"); pStr != "" { if parsed, err := strconv.ParseInt(pStr, 10, 64); err == nil { projectID = parsed } } list, err := s.freeDoc().ListDocuments(r.Context(), userID, projectID, space) if err != nil { writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "error_code": "LIST_FAILED", "message": err.Error(), }) return } if list == nil { list = []freedoc.FreeformDocSummary{} } writeJSONResponse(w, http.StatusOK, map[string]any{ "code": 0, "data": list, }) } // GET /api/v1/freeform-documents/{doc_id} func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } docID := extractDocIDFromPath(r, "") if docID == "" { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_DOC_ID", "message": "缺少文档ID", }) return } detail, err := s.freeDoc().GetDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{ "error_code": "DOC_NOT_FOUND", "message": "文档不存在或已被删除", }) return } if errors.Is(err, freedoc.ErrForbidden) { writeJSONResponse(w, http.StatusForbidden, map[string]any{ "error_code": "DOC_ACCESS_FORBIDDEN", "message": "该文档为他人私密文档,严格禁止跨用户访问", }) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "error_code": "GET_FAILED", "message": err.Error(), }) return } writeJSONResponse(w, http.StatusOK, map[string]any{ "code": 0, "data": detail, }) } // PUT /api/v1/freeform-documents/{doc_id} func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } docID := extractDocIDFromPath(r, "") if docID == "" { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_DOC_ID", "message": "缺少文档ID", }) return } var req freedoc.UpdateFreeformDocRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_BODY", "message": "请求体格式错误", }) return } // 优先提取 HTTP If-Match 标头作为乐观并发校验 if ifMatch := r.Header.Get("If-Match"); ifMatch != "" { cleanMatch := strings.Trim(ifMatch, "\"") if v, err := strconv.Atoi(cleanMatch); err == nil && v > 0 { req.ExpectedVersion = &v } else { req.ExpectedSHA256 = &cleanMatch } } detail, err := s.freeDoc().UpdateDocument(r.Context(), userID, docID, req) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{ "error_code": "DOC_NOT_FOUND", "message": "文档不存在", }) return } if errors.Is(err, freedoc.ErrForbidden) { writeJSONResponse(w, http.StatusForbidden, map[string]any{ "error_code": "DOC_ACCESS_FORBIDDEN", "message": "无权修改此文档", }) return } if errors.Is(err, freedoc.ErrConflict) { writeJSONResponse(w, http.StatusConflict, map[string]any{ "error_code": "CONCURRENCY_CONFLICT", "message": "文档已被他人或新版本更新,请刷新重试", }) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "error_code": "UPDATE_FAILED", "message": err.Error(), }) return } writeJSONResponse(w, http.StatusOK, map[string]any{ "code": 0, "message": "保存成功", "data": detail, }) } // POST /api/v1/freeform-documents/{doc_id}/share func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } docID := extractDocIDFromPath(r, "share") if docID == "" { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_DOC_ID", "message": "缺少文档ID", }) return } doc, err := s.freeDoc().ShareDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) return } if errors.Is(err, freedoc.ErrForbidden) { writeJSONResponse(w, http.StatusForbidden, map[string]any{"error_code": "FORBIDDEN", "message": "仅文档所有者可共享文档"}) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{"error_code": "SHARE_FAILED", "message": err.Error()}) return } writeJSONResponse(w, http.StatusOK, map[string]any{ "code": 0, "message": "已共享至项目参考库", "data": doc, }) } // POST /api/v1/freeform-documents/{doc_id}/unshare func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } docID := extractDocIDFromPath(r, "unshare") if docID == "" { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_DOC_ID", "message": "缺少文档ID", }) return } doc, err := s.freeDoc().UnshareDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) return } if errors.Is(err, freedoc.ErrForbidden) { writeJSONResponse(w, http.StatusForbidden, map[string]any{"error_code": "FORBIDDEN", "message": "仅文档所有者可撤回共享"}) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{"error_code": "UNSHARE_FAILED", "message": err.Error()}) return } writeJSONResponse(w, http.StatusOK, map[string]any{ "code": 0, "message": "已撤回为个人私密", "data": doc, }) } // DELETE /api/v1/freeform-documents/{doc_id} func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) { if s.freeDoc() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized") return } userID, ok := s.requireUserID(w, r) if !ok { return } docID := extractDocIDFromPath(r, "") if docID == "" { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ "error_code": "INVALID_DOC_ID", "message": "缺少文档ID", }) return } err := s.freeDoc().DeleteDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) return } if errors.Is(err, freedoc.ErrForbidden) { writeJSONResponse(w, http.StatusForbidden, map[string]any{"error_code": "FORBIDDEN", "message": "仅文档所有者可删除"}) return } writeJSONResponse(w, http.StatusInternalServerError, map[string]any{"error_code": "DELETE_FAILED", "message": err.Error()}) return } writeJSONResponse(w, http.StatusOK, map[string]any{ "code": 0, "message": "已移至回收站", }) }