284 lines
12 KiB
Go
284 lines
12 KiB
Go
package docgov
|
|
|
|
import (
|
|
"time"
|
|
|
|
"chat-agent-system/internal/contracts/vocabulary"
|
|
)
|
|
|
|
// Re-export stable vocabulary so domain code keeps short names.
|
|
// Cross-package consumers should import contracts/vocabulary for State/Role/Err.
|
|
type (
|
|
DocumentState = vocabulary.DocumentState
|
|
UserRole = vocabulary.UserRole
|
|
LeaseHolderType = vocabulary.LeaseHolderType
|
|
CommentType = vocabulary.CommentType
|
|
)
|
|
|
|
const (
|
|
StateDrafting = vocabulary.StateDrafting
|
|
StateReviewing = vocabulary.StateReviewing
|
|
StateApproving = vocabulary.StateApproving
|
|
StateApproved = vocabulary.StateApproved
|
|
StateRejected = vocabulary.StateRejected
|
|
|
|
RoleAuthor = vocabulary.RoleAuthor
|
|
RoleReviewer = vocabulary.RoleReviewer
|
|
RoleApprover = vocabulary.RoleApprover
|
|
RoleSystem = vocabulary.RoleSystem
|
|
|
|
LeaseHolderNone = vocabulary.LeaseHolderNone
|
|
LeaseHolderSquad = vocabulary.LeaseHolderSquad
|
|
LeaseHolderHuman = vocabulary.LeaseHolderHuman
|
|
|
|
DefaultSquadLeaseTTL = vocabulary.DefaultSquadLeaseTTL
|
|
DefaultHumanLeaseTTL = vocabulary.DefaultHumanLeaseTTL
|
|
|
|
CommentGeneral = vocabulary.CommentGeneral
|
|
CommentSuggestion = vocabulary.CommentSuggestion
|
|
CommentRejection = vocabulary.CommentRejection
|
|
|
|
ErrCodeDocumentNotFound = vocabulary.ErrCodeDocumentNotFound
|
|
ErrCodeProjectionMissing = vocabulary.ErrCodeProjectionMissing
|
|
ErrCodeProjectionEmpty = vocabulary.ErrCodeProjectionEmpty
|
|
ErrCodeIntegrityMismatch = vocabulary.ErrCodeIntegrityMismatch
|
|
ErrCodeProjectionDrift = vocabulary.ErrCodeProjectionDrift
|
|
)
|
|
|
|
var (
|
|
ErrDocumentNotFound = vocabulary.ErrDocumentNotFound
|
|
ErrDocumentLocked = vocabulary.ErrDocumentLocked
|
|
ErrOnlyAuthorCanEdit = vocabulary.ErrOnlyAuthorCanEdit
|
|
ErrReviewerCannotEdit = vocabulary.ErrReviewerCannotEdit
|
|
ErrApproverCannotEdit = vocabulary.ErrApproverCannotEdit
|
|
ErrInvalidStateTransition = vocabulary.ErrInvalidStateTransition
|
|
ErrRejectReasonRequired = vocabulary.ErrRejectReasonRequired
|
|
ErrUnauthorizedActor = vocabulary.ErrUnauthorizedActor
|
|
ErrProposalNotFound = vocabulary.ErrProposalNotFound
|
|
ErrProposalAlreadyDecided = vocabulary.ErrProposalAlreadyDecided
|
|
ErrChangeProposalPending = vocabulary.ErrChangeProposalPending
|
|
ErrDocumentBusy = vocabulary.ErrDocumentBusy
|
|
ErrStaleFencing = vocabulary.ErrStaleFencing
|
|
ErrLeaseExpired = vocabulary.ErrLeaseExpired
|
|
ErrLeaseNotHeld = vocabulary.ErrLeaseNotHeld
|
|
ErrForceEditNotAllowed = vocabulary.ErrForceEditNotAllowed
|
|
ErrContentProjectionMissing = vocabulary.ErrContentProjectionMissing
|
|
ErrContentProjectionEmpty = vocabulary.ErrContentProjectionEmpty
|
|
ErrContentIntegrityMismatch = vocabulary.ErrContentIntegrityMismatch
|
|
)
|
|
|
|
// ContentError is a structured content-read failure. 404 is only for a missing record.
|
|
type ContentError struct {
|
|
Code string
|
|
Message string
|
|
Retryable bool
|
|
NeedsAdmin bool
|
|
RecordSHA string
|
|
FileSHA string
|
|
HeadSHA string
|
|
Class string
|
|
cause error
|
|
}
|
|
|
|
func (e *ContentError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if e.Message != "" {
|
|
return e.Message
|
|
}
|
|
return e.Code
|
|
}
|
|
|
|
func (e *ContentError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.cause
|
|
}
|
|
|
|
func (e *ContentError) ErrorCode() string {
|
|
if e == nil || e.Code == "" {
|
|
return "DOCUMENT_CONTENT_ERROR"
|
|
}
|
|
return e.Code
|
|
}
|
|
|
|
func NewContentError(code string, cause error, retryable, needsAdmin bool) *ContentError {
|
|
msg := code
|
|
if cause != nil {
|
|
msg = cause.Error()
|
|
}
|
|
return &ContentError{Code: code, Message: msg, Retryable: retryable, NeedsAdmin: needsAdmin, cause: cause}
|
|
}
|
|
|
|
// DocumentBusyError 携带租约占用详情,便于 HTTP 409 与 UI 提示
|
|
type DocumentBusyError struct {
|
|
DocID string `json:"doc_id"`
|
|
HolderType LeaseHolderType `json:"holder_type"`
|
|
HolderID string `json:"holder_id"`
|
|
HolderDisplayName string `json:"holder_display_name,omitempty"`
|
|
WorkDir string `json:"work_dir,omitempty"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
func (e *DocumentBusyError) Error() string {
|
|
if e.Reason != "" {
|
|
return e.Reason
|
|
}
|
|
return ErrDocumentBusy.Error()
|
|
}
|
|
|
|
func (e *DocumentBusyError) Unwrap() error { return ErrDocumentBusy }
|
|
|
|
func (e *DocumentBusyError) ErrorCode() string { return "DOC_BUSY" }
|
|
|
|
// DocumentConfig 受控文档流转与退回配置
|
|
type DocumentConfig struct {
|
|
RejectRollbackTo DocumentState `json:"reject_rollback_to"` // 驳回退回目标: drafting 或 reviewing
|
|
AllowApproverToReviewer bool `json:"allow_approver_to_reviewer"` // 是否允许批准人退回至审核阶段
|
|
RequireAuthorConfirmOnReject bool `json:"require_author_confirm_on_reject"` // 驳回退回编制时是否需编制人确认已获悉
|
|
}
|
|
|
|
func DefaultDocumentConfig() DocumentConfig {
|
|
return DocumentConfig{
|
|
RejectRollbackTo: StateDrafting,
|
|
AllowApproverToReviewer: true,
|
|
RequireAuthorConfirmOnReject: true,
|
|
}
|
|
}
|
|
|
|
// DocumentRecord 受控文档治理台账实体
|
|
type DocumentRecord struct {
|
|
ID int64 `json:"id"`
|
|
DocID string `json:"doc_id"` // 业务唯一ID,如 doc_proj1_feasibility_reg_v1
|
|
ProjectID int64 `json:"project_id"` // 项目ID
|
|
TaskKey string `json:"task_key"` // 任务标识: feasibility_reg, intake, cdo_quote
|
|
Title string `json:"title"` // 标题
|
|
Stage string `json:"stage"` // 阶段: V0.5_feasibility
|
|
Version int `json:"version"` // 版本号
|
|
CurrentState DocumentState `json:"current_state"` // drafting, reviewing, approving, approved
|
|
IsLocked bool `json:"is_locked"` // reviewing/approving/approved 期间锁定
|
|
IsTemplateOnly bool `json:"is_template_only"` // 是否仅为骨架模板 (模板状态即使查看也不算认领)
|
|
ContentSHA256 string `json:"content_sha256"` // 当前版本正文哈希
|
|
WorkDir string `json:"work_dir"` // 关联工作区相对路径
|
|
|
|
// 三级法定责任人 (各有且仅有一个)
|
|
AuthorID int64 `json:"author_id"`
|
|
AuthorName string `json:"author_name"`
|
|
ReviewerID int64 `json:"reviewer_id"`
|
|
ReviewerName string `json:"reviewer_name"`
|
|
ApproverID int64 `json:"approver_id"`
|
|
ApproverName string `json:"approver_name"`
|
|
|
|
// DutyLocked 为 true 时,职责权威来自工作流实例规划,heal/revoke 不得覆盖。
|
|
DutyLocked bool `json:"duty_locked"`
|
|
DutyPolicy string `json:"duty_policy,omitempty"`
|
|
|
|
// 认领状态 (Claiming State)
|
|
AuthorClaimed bool `json:"author_claimed"` // 编制人是否已认领 (非模板状态查看/编辑后为 true)
|
|
AuthorClaimedAt *time.Time `json:"author_claimed_at"`
|
|
AuthorLastViewedAt *time.Time `json:"author_last_viewed_at"`
|
|
AuthorLastEditedAt *time.Time `json:"author_last_edited_at"`
|
|
ReviewerClaimed bool `json:"reviewer_claimed"` // 审核人是否已认领审核
|
|
ReviewerClaimedAt *time.Time `json:"reviewer_claimed_at"`
|
|
ApproverClaimed bool `json:"approver_claimed"` // 批准人是否已认领批准
|
|
ApproverClaimedAt *time.Time `json:"approver_claimed_at"`
|
|
|
|
// 驳回与决议记录
|
|
LastRejectedReason string `json:"last_rejected_reason"`
|
|
LastRejectedByRole string `json:"last_rejected_by_role"`
|
|
LastRejectedByName string `json:"last_rejected_by_name"`
|
|
LastRejectedAt *time.Time `json:"last_rejected_at"`
|
|
ApprovedAt *time.Time `json:"approved_at"`
|
|
ReviewSnapshotID int64 `json:"review_snapshot_id,omitempty"`
|
|
|
|
// 编辑租约 (运行时互斥,正交于 IsLocked)
|
|
LeaseHolderType LeaseHolderType `json:"lease_holder_type,omitempty"`
|
|
LeaseHolderID string `json:"lease_holder_id,omitempty"`
|
|
LeaseWorkDir string `json:"lease_work_dir,omitempty"`
|
|
LeaseExpiresAt *time.Time `json:"lease_expires_at,omitempty"`
|
|
LeaseHeartbeatAt *time.Time `json:"lease_heartbeat_at,omitempty"`
|
|
LeaseFencingToken string `json:"lease_fencing_token,omitempty"`
|
|
|
|
Config DocumentConfig `json:"config"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// HasActiveLease 判断文档是否持有未过期的编辑租约
|
|
func (d *DocumentRecord) HasActiveLease(now time.Time) bool {
|
|
if d == nil || d.LeaseHolderType == LeaseHolderNone || d.LeaseHolderType == "" {
|
|
return false
|
|
}
|
|
if d.LeaseExpiresAt == nil {
|
|
return false
|
|
}
|
|
return d.LeaseExpiresAt.After(now)
|
|
}
|
|
|
|
// ClearLease 清空租约字段(内存)
|
|
func (d *DocumentRecord) ClearLease() {
|
|
d.LeaseHolderType = LeaseHolderNone
|
|
d.LeaseHolderID = ""
|
|
d.LeaseWorkDir = ""
|
|
d.LeaseExpiresAt = nil
|
|
d.LeaseHeartbeatAt = nil
|
|
d.LeaseFencingToken = ""
|
|
}
|
|
|
|
// DocumentComment 批注与修改意见实体
|
|
type DocumentComment struct {
|
|
ID int64 `json:"id"`
|
|
DocID string `json:"doc_id"`
|
|
UserID int64 `json:"user_id"`
|
|
UserName string `json:"user_name"`
|
|
UserRole UserRole `json:"user_role"` // author, reviewer, approver
|
|
TargetSection string `json:"target_section"` // 目标章节锚点,如 "## 结论"
|
|
CommentType CommentType `json:"comment_type"` // comment, suggestion, reject_reason
|
|
Content string `json:"content"` // 意见内容
|
|
Status string `json:"status"` // open, resolved, accepted
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// ProposalStatus 变更提案状态
|
|
type ProposalStatus string
|
|
|
|
const (
|
|
ProposalPendingAuthorApproval ProposalStatus = "pending_author_approval" // 待编制人拍板
|
|
ProposalApprovedByAuthor ProposalStatus = "approved_by_author" // 编制人已同意修改
|
|
ProposalRejectedByAuthor ProposalStatus = "rejected_by_author" // 编制人已拒绝修改
|
|
ProposalApplied ProposalStatus = "applied" // 已应用执行
|
|
)
|
|
|
|
// ChangeProposal 针对已认领文档的智能体修改变更提案实体
|
|
type ChangeProposal struct {
|
|
ID int64 `json:"id"`
|
|
ProposalID string `json:"proposal_id"`
|
|
DocID string `json:"doc_id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
TriggeredBy string `json:"triggered_by"` // agent_squad, stage_sync, upstream_change
|
|
ProposedChanges string `json:"proposed_changes"` // 有哪些地方要改 (变更内容与对比摘要)
|
|
ImpactAnalysis string `json:"impact_analysis"` // 影响会是什么 (范围、上下游依赖)
|
|
RiskAssessment string `json:"risk_assessment"` // 可能产生的风险 (合规、工时、预算)
|
|
Status ProposalStatus `json:"status"` // pending_author_approval...
|
|
AuthorDecisionReason string `json:"author_decision_reason"` // 编制人批准/拒绝原因
|
|
DecidedAt *time.Time `json:"decided_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// AuditLog 文档治理生命周期审计跟踪
|
|
type AuditLog struct {
|
|
ID int64 `json:"id"`
|
|
DocID string `json:"doc_id"`
|
|
Action string `json:"action"` // view, edit, claim, submit_review, review_approve, review_reject...
|
|
ActorID int64 `json:"actor_id"`
|
|
ActorName string `json:"actor_name"`
|
|
ActorRole UserRole `json:"actor_role"`
|
|
FromState DocumentState `json:"from_state"`
|
|
ToState DocumentState `json:"to_state"`
|
|
Details map[string]any `json:"details"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|