Files
Agent-System/internal/app/docnotify/lifecycle.go
T

172 lines
5.5 KiB
Go

package docnotify
import (
"context"
"fmt"
"strings"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/notification"
)
// LifecycleAction identifies a governance transition for routing.
type LifecycleAction string
const (
LifecycleSubmitReview LifecycleAction = "submit_review"
LifecycleReviewApprove LifecycleAction = "review_approve"
LifecycleReviewReject LifecycleAction = "review_reject"
LifecycleApproverApprove LifecycleAction = "approver_approve"
LifecycleApproverReject LifecycleAction = "approver_reject"
LifecycleReopen LifecycleAction = "reopen"
)
// RecipientForDocEvent resolves the duty-holder inbox recipient for a document event.
func RecipientForDocEvent(doc *docgov.DocumentRecord, eventType string) int64 {
if doc == nil {
return 0
}
switch eventType {
case notification.EventDocReviewRequested:
if doc.ReviewerID > 0 {
return doc.ReviewerID
}
case notification.EventDocApprovalRequested:
if doc.ApproverID > 0 {
return doc.ApproverID
}
case notification.EventDocRejected, notification.EventDocReopened, notification.EventDocPendingSupplements:
if doc.AuthorID > 0 {
return doc.AuthorID
}
case notification.EventDocApproved:
if doc.AuthorID > 0 {
return doc.AuthorID
}
}
if doc.AuthorID > 0 {
return doc.AuthorID
}
return 0
}
// EmitLifecycleParams carries optional chat side channels.
type EmitLifecycleParams struct {
Notify notificationEmitter
Digest digestUpserter
Chat *ctxmgr.ContextManager
Action LifecycleAction
Doc *docgov.DocumentRecord
ActorID int64
ActorName string
Detail string
}
type digestUpserter interface {
UpsertDocumentDigest(ctx context.Context, p notification.UpsertDigestParams) error
}
// EmitLifecycle notifies the duty holder for a governance transition.
func EmitLifecycle(ctx context.Context, p EmitLifecycleParams) {
if p.Doc == nil || p.Notify == nil || !p.Notify.Available() {
return
}
eventType, notifType, title, body, severity := lifecycleCopy(p.Action, p.Doc, p.ActorName, p.Detail)
if eventType == "" {
return
}
recipient := RecipientForDocEvent(p.Doc, eventType)
if recipient <= 0 {
return
}
payload := notification.DocumentEventPayload{
ProjectID: p.Doc.ProjectID, DocID: p.Doc.DocID, DocTitle: p.Doc.Title,
Summary: truncateLifecycle(p.Detail, 80),
}
dedupe := fmt.Sprintf("doc_lifecycle:%s:%s:%d:%s", p.Action, p.Doc.DocID, recipient, p.Doc.CurrentState)
_, _ = p.Notify.EmitDocumentEvent(ctx, eventType, notifType, title, body, dedupe, recipient, payload)
if p.Digest != nil && severity == notification.SeverityActionRequired {
kind := digestKindForEvent(eventType)
if kind != "" {
_ = p.Digest.UpsertDocumentDigest(ctx, notification.UpsertDigestParams{
UserID: recipient, ProjectID: p.Doc.ProjectID, DigestKind: kind,
DocID: p.Doc.DocID, DocTitle: p.Doc.Title, Severity: severity,
ActorID: p.ActorID, ActorName: p.ActorName, Summary: truncateLifecycle(p.Detail, 60),
})
}
}
if p.Chat != nil {
_ = PostProjectNotice(ctx, p.Chat, p.Doc.ProjectID, recipient, body, map[string]any{
"doc_id": p.Doc.DocID, "action": string(p.Action), "event": eventType,
})
}
}
func digestKindForEvent(eventType string) string {
switch eventType {
case notification.EventDocReviewRequested:
return notification.DigestKindReviewRequested
case notification.EventDocApprovalRequested:
return notification.DigestKindApprovalRequested
case notification.EventDocPendingSupplements:
return notification.DigestKindPendingSupplements
default:
return ""
}
}
func lifecycleCopy(action LifecycleAction, doc *docgov.DocumentRecord, actorName, detail string) (eventType, notifType, title, body, severity string) {
titleDoc := strings.TrimSpace(doc.Title)
if titleDoc == "" {
titleDoc = doc.DocID
}
actor := strings.TrimSpace(actorName)
if actor == "" {
actor = "成员"
}
reason := strings.TrimSpace(detail)
switch action {
case LifecycleSubmitReview:
return notification.EventDocReviewRequested, notification.TypeDocReviewRequested,
fmt.Sprintf("《%s》待您审核", titleDoc),
fmt.Sprintf("%s 已送审,请审核本文档。", actor),
notification.SeverityActionRequired
case LifecycleReviewApprove:
return notification.EventDocApprovalRequested, notification.TypeDocApprovalRequested,
fmt.Sprintf("《%s》待您批准", titleDoc),
fmt.Sprintf("%s 审核已通过,请批准本文档。", actor),
notification.SeverityActionRequired
case LifecycleReviewReject, LifecycleApproverReject:
msg := fmt.Sprintf("%s 驳回了《%s》。", actor, titleDoc)
if reason != "" {
msg += "原因:" + reason
}
return notification.EventDocRejected, notification.TypeDocRejected,
fmt.Sprintf("《%s》已被驳回", titleDoc), msg,
notification.SeverityActionRequired
case LifecycleApproverApprove:
return notification.EventDocApproved, notification.TypeDocApproved,
fmt.Sprintf("《%s》已批准归档", titleDoc),
fmt.Sprintf("%s 已终审批准,《%s》已归档锁定。", actor, titleDoc),
notification.SeverityInfo
case LifecycleReopen:
return notification.EventDocReopened, notification.TypeDocReopened,
fmt.Sprintf("《%s》已重开修订", titleDoc),
fmt.Sprintf("%s 批准重开修订,《%s》已回编制期。", actor, titleDoc),
notification.SeverityActionRequired
default:
return "", "", "", "", ""
}
}
func truncateLifecycle(s string, n int) string {
r := []rune(strings.TrimSpace(s))
if len(r) <= n {
return string(r)
}
return string(r[:n]) + "…"
}