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

159 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"encoding/json"
"fmt"
"net/http"
"time"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectinstance"
)
// -----------------------------------------------------------------------------
// 文档正文保存与编辑租约
// -----------------------------------------------------------------------------
func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
var req SaveContentRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeDomainError(w, r, http.StatusBadRequest, err)
return
}
if s.gov() == nil {
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
// 成员级 DocAccessActionEdit(deny_only_submit);送审仍走 submit 职责门禁。
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionEdit) {
return
}
role := vocabulary.RoleAuthor
res, err := s.gov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
DocID: req.DocID,
ActorID: user.ID,
ActorRole: role,
ActorName: user.Name,
ActorIsMember: true, // enforceDocGovWorkflowAction 已确认成员可 edit
MarkdownBody: req.MarkdownBody,
ExpectedSHA256: req.ExpectedSHA256,
FencingToken: req.FencingToken,
})
if err != nil {
writeDocGovError(w, r, err)
return
}
writeJSONResponse(w, http.StatusOK, res)
}
func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
var req LeaseRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeDomainError(w, r, http.StatusBadRequest, err)
return
}
if s.gov() == nil {
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderHuman {
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionEdit) {
return
}
if err := s.gov().CheckHumanCanEdit(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, true); err != nil {
writeDocGovError(w, r, err)
return
}
req.HolderID = fmt.Sprintf("%d", user.ID)
}
workDir, werr := s.resolver().ResolveDocWorkDir(r.Context(), req.DocID)
if werr != nil {
workDir = ""
}
ttl := time.Duration(req.TTLSeconds) * time.Second
doc, err := s.gov().AcquireLease(r.Context(), docgov.LeaseAcquireRequest{
DocID: req.DocID,
HolderType: vocabulary.LeaseHolderType(req.HolderType),
HolderID: req.HolderID,
WorkDir: workDir,
TTL: ttl,
})
if err != nil {
writeDocGovError(w, r, err)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
var req LeaseRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeDomainError(w, r, http.StatusBadRequest, err)
return
}
if s.gov() == nil {
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderHuman {
req.HolderID = fmt.Sprintf("%d", user.ID)
} else if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderSquad {
if !s.callerOwnsSquadLease(user.ID, req.HolderID) {
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "ERR_FORBIDDEN", "message": "无权续租该文档编辑锁",
})
return
}
}
ttl := time.Duration(req.TTLSeconds) * time.Second
doc, err := s.gov().HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl)
if err != nil {
writeDocGovError(w, r, err)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}
func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
var req LeaseRequestDTO
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeDomainError(w, r, http.StatusBadRequest, err)
return
}
if s.gov() == nil {
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderHuman {
req.HolderID = fmt.Sprintf("%d", user.ID)
} else if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderSquad {
if !s.callerOwnsSquadLease(user.ID, req.HolderID) {
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "ERR_FORBIDDEN", "message": "无权释放该文档编辑锁",
})
return
}
}
doc, err := s.gov().ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID)
if err != nil {
writeDocGovError(w, r, err)
return
}
writeJSONResponse(w, http.StatusOK, doc)
}