package docnotify import ( "context" "encoding/json" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/docgov" "chat-agent-system/internal/events" "chat-agent-system/internal/notification" "github.com/jackc/pgx/v5" ) // EventConsumer is the durable notify worker. Closures remain the unique writer until live cutover. type EventConsumer struct { Notify *notification.Service Docs DocumentLookup Chat *ctxmgr.ContextManager Env string } func (c *EventConsumer) HandleLease(ctx context.Context, lease events.Lease, tx pgx.Tx) error { if c == nil || c.Notify == nil { return events.ErrUnknownConsumer } principal := events.ServicePrincipal{ ID: "notify-" + c.Env, Environment: c.Env, Consumer: events.ConsumerNotify, Actions: []string{"notify.emit"}, } if err := events.AuthorizePrincipal(principal, lease.Environment, lease.ProjectID, "notify.emit"); err != nil { return events.ErrQuarantine } kind, title, body := notifyCopy(lease.EventName) if kind == "" { return nil } docID := lease.Envelope.AggregateID var payload map[string]any _ = json.Unmarshal(lease.Envelope.Payload, &payload) if payload != nil { if id, ok := payload["doc_id"].(string); ok && id != "" { docID = id } } var recipient int64 projectID := lease.ProjectID var doc *docgov.DocumentRecord if c.Docs != nil && docID != "" { d, err := c.Docs.GetDocument(ctx, docID) if err != nil || d == nil { return events.ErrQuarantine } if d.ProjectID != lease.ProjectID { return events.ErrQuarantine } doc = d projectID = d.ProjectID } recipient = RecipientForDocEvent(doc, lease.EventName) if recipient <= 0 && doc != nil { recipient = doc.AuthorID } if recipient <= 0 { return events.ErrQuarantine } p := notification.DocumentEventPayload{ProjectID: projectID, DocID: docID} if err := c.Notify.EmitVisibleDocumentEvent(ctx, tx, lease.EventID, kind, title, body, recipient, p); err != nil { return err } if lease.EventName == events.EventDownstreamImpact && c.Chat != nil { impacted, _ := payload["impacted_doc_id"].(string) _ = PostProjectNotice(ctx, c.Chat, projectID, recipient, downstreamChatBody(docID, impacted), nil) } return nil } func IntendedNotifyDigest(eventID, kind string, recipient int64) string { return events.VisibleOutcomeDigest(eventID, kind, recipient) } func notifyCopy(eventName string) (kind, title, body string) { switch eventName { case events.EventCandidateReady: return notification.TypeDocAgentCandidateReady, "智能体变更待确认", "文档有高风险智能体候选变更,请确认后生效。" case events.EventVersionCommitted: return notification.TypeDocAgentVersionCommitted, "智能体版本已生效", "新的智能体文档版本已提交。" case events.EventDownstreamImpact: return notification.TypeDocDownstreamImpact, "下游文档受影响", "上游文档变更,请复核关联文档。" case events.EventChangeReportDegraded: return notification.TypeDocChangeReportDegraded, "变更报告未完整评估", "变更报告降级,受控文档暂不可送审。" case events.EventApproved: return notification.TypeDocApproved, "文档已批准", "文档已完成终审批准。" case events.EventDocProposalRequired: return notification.TypeDocProposalRequired, "文档变更待确认", "已认领文档有变更提案,请确认后由您发起修改。" case events.EventDocUnclaimedEdit: return notification.TypeDocUnclaimedEditApplied, "文档已更新", "未认领文档已被直接更新(无需您确认)。" case notification.EventDocReviewRequested: return notification.TypeDocReviewRequested, "文档待审核", "有文档已送审,请审核。" case notification.EventDocApprovalRequested: return notification.TypeDocApprovalRequested, "文档待批准", "审核已通过,请批准。" case notification.EventDocRejected: return notification.TypeDocRejected, "文档已驳回", "文档已被驳回,请查看原因并修订。" case notification.EventDocReopened: return notification.TypeDocReopened, "文档已重开", "文档已重开修订,请继续编制。" default: return "", "", "" } }