Files
Agent-System/internal/app/docnotify/subscriber.go
T
weichen 2e56ba9719 feat(docgov): complete review lifecycle gaps for lock/notify/comment/reopen
Defer locked-period supplements to a pending ledger, route duty-holder
notifications, persist stage-scoped comments, and prefer Approver for reopen.
2026-09-25 20:08:56 +00:00

133 lines
5.0 KiB
Go

package docnotify
import (
"context"
"fmt"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/notification"
)
// DocumentLookup resolves a document for recipient and project fill-in.
type DocumentLookup interface {
GetDocument(ctx context.Context, docID string) (*docgov.DocumentRecord, error)
}
// Subscriber is the unique consumer for document OnOutbox notification events.
// Bind once from the app composition root; do not assign OnOutbox in server.
type Subscriber struct {
Notify notificationEmitter
Docs DocumentLookup
Chat *ctxmgr.ContextManager
// SkipWrite is true when the durable notify consumer is live.
// Bind stays registered while mode=off; the closure must not dual-write after switch.
SkipWrite func(ctx context.Context) bool
}
type notificationEmitter interface {
Available() bool
EmitDocumentEvent(ctx context.Context, eventType, notifType, title, body, dedupeKey string, recipientUserID int64, payload notification.DocumentEventPayload) (bool, error)
}
// Bind assigns the subscriber exactly once. Repeat calls keep the first handler.
func Bind(rev *docrevision.Service, sub *Subscriber) bool {
if rev == nil || sub == nil || rev.OnOutbox != nil {
return false
}
rev.OnOutbox = sub.Handle
return true
}
// Handle maps one document outbox event onto a permission-safe inbox notification.
func (s *Subscriber) Handle(ctx context.Context, eventType string, payload map[string]any) error {
if s == nil {
return nil
}
if s.SkipWrite != nil && s.SkipWrite(ctx) {
return nil
}
if s.Notify == nil || !s.Notify.Available() {
return nil
}
docID, _ := payload["doc_id"].(string)
projectID, _ := payload["project_id"].(int64)
risk, _ := payload["risk"].(string)
var doc *docgov.DocumentRecord
if s.Docs != nil && docID != "" {
if d, err := s.Docs.GetDocument(ctx, docID); err == nil && d != nil {
doc = d
projectID = d.ProjectID
}
}
recipient := RecipientForDocEvent(doc, eventType)
if recipient == 0 && doc != nil {
recipient = doc.AuthorID
}
if recipient == 0 {
return nil
}
p := notification.DocumentEventPayload{
ProjectID: projectID, DocID: docID, RiskLevel: risk,
}
switch eventType {
case notification.EventDocAgentCandidateReady:
if v, ok := payload["candidate_id"].(int64); ok {
p.CandidateID = v
}
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocAgentCandidateReady,
"智能体变更待确认", "文档有高风险智能体候选变更,请确认后生效。",
fmt.Sprintf("doc_candidate:%s:%d:%d", docID, p.CandidateID, recipient), recipient, p)
return err
case notification.EventDocAgentVersionCommitted:
if v, ok := payload["version_no"].(int); ok {
p.VersionNo = v
}
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocAgentVersionCommitted,
"智能体版本已生效", "新的智能体文档版本已提交。",
fmt.Sprintf("doc_agent_version:%s:%d:%d", docID, p.VersionNo, recipient), recipient, p)
return err
case notification.EventDocDownstreamImpact:
impacted, _ := payload["impacted_doc_id"].(string)
p.ImpactedDocID = impacted
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocDownstreamImpact,
"下游文档受影响", "上游文档变更,请复核关联文档。",
fmt.Sprintf("doc_impact:%s:%s:%d", docID, impacted, recipient), recipient, p)
if err != nil {
return err
}
return PostProjectNotice(ctx, s.Chat, projectID, recipient, downstreamChatBody(docID, impacted), nil)
case notification.EventDocChangeReportDegraded:
if v, ok := payload["report_id"].(int64); ok {
p.ReportID = v
}
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocChangeReportDegraded,
"变更报告未完整评估", "变更报告降级,受控文档暂不可送审。",
fmt.Sprintf("doc_report_degraded:%s:%d:%d", docID, p.ReportID, recipient), recipient, p)
return err
case notification.EventDocReviewRequested:
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocReviewRequested,
"文档待审核", "有文档已送审,请审核。",
fmt.Sprintf("doc_review_requested:%s:%d", docID, recipient), recipient, p)
return err
case notification.EventDocApprovalRequested:
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocApprovalRequested,
"文档待批准", "审核已通过,请批准。",
fmt.Sprintf("doc_approval_requested:%s:%d", docID, recipient), recipient, p)
return err
case notification.EventDocRejected:
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocRejected,
"文档已驳回", "文档已被驳回,请查看原因并修订。",
fmt.Sprintf("doc_rejected:%s:%d", docID, recipient), recipient, p)
return err
case notification.EventDocReopened:
_, err := s.Notify.EmitDocumentEvent(ctx, eventType, notification.TypeDocReopened,
"文档已重开", "文档已重开修订,请继续编制。",
fmt.Sprintf("doc_reopened:%s:%d", docID, recipient), recipient, p)
return err
default:
return nil
}
}