Files
Agent-System/internal/app/docnotify/consumer.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

110 lines
4.1 KiB
Go

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 "", "", ""
}
}