From 4bfe1cb5ad9ba956eab1812547db63a314dc4d6c Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 12 Sep 2026 03:14:30 +0000 Subject: [PATCH] feat(api): wire document approval hook and project notifications API. Register workflow approval callback on server start and expose GET /projects/{id}/notifications. Co-authored-by: Cursor --- internal/server/notification_handlers.go | 35 ++++++++++++++++++ internal/server/server.go | 5 +++ internal/server/workflow_approval_hook.go | 45 +++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 internal/server/notification_handlers.go create mode 100644 internal/server/workflow_approval_hook.go diff --git a/internal/server/notification_handlers.go b/internal/server/notification_handlers.go new file mode 100644 index 00000000..157210c4 --- /dev/null +++ b/internal/server/notification_handlers.go @@ -0,0 +1,35 @@ +package server + +import ( + "net/http" + "strconv" +) + +func (s *Server) handleListProjectNotifications(w http.ResponseWriter, r *http.Request) { + user, ok := s.requireAuthenticatedUser(w, r) + if !ok { + return + } + if s.notifySvc == nil || !s.notifySvc.Available() { + http.Error(w, "notification service unavailable", http.StatusServiceUnavailable) + return + } + + projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || projectID <= 0 { + http.Error(w, "invalid project id", http.StatusBadRequest) + return + } + + sinceID, _ := strconv.ParseInt(r.URL.Query().Get("since_id"), 10, 64) + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + + items, err := s.notifySvc.ListProjectNotifications(r.Context(), user.ID, projectID, sinceID, limit) + if err != nil { + http.Error(w, "list notifications failed: "+err.Error(), http.StatusInternalServerError) + return + } + writeJSONResponse(w, http.StatusOK, map[string]any{ + "items": items, + }) +} diff --git a/internal/server/server.go b/internal/server/server.go index ed6dcbe5..8530ba81 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -19,6 +19,7 @@ import ( "chat-agent-system/internal/coordinator" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/intent" + "chat-agent-system/internal/notification" "chat-agent-system/internal/observability" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectworkflow" @@ -42,6 +43,7 @@ type Server struct { authzSvc *authz.Service wfDefSvc *workflowdef.Service instSvc *projectinstance.Service + notifySvc *notification.Service resolver *workspace.Resolver httpServer *http.Server } @@ -97,8 +99,10 @@ func NewServer(cfg *config.Config, svc *bizdev.Service) *Server { authzSvc: authzSvc, wfDefSvc: wfDefSvc, instSvc: instSvc, + notifySvc: notification.NewService(pool), resolver: workspace.NewResolver(cfg.DataDir, pool), } + s.registerWorkflowApprovalHook() s.initTaskRunExecutors() return s } @@ -149,6 +153,7 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("GET /api/v1/projects/{id}/workflows/{workflow_key}/runs", s.handleListProjectWorkflowRuns) mux.HandleFunc("GET /api/v1/projects/{id}/lineage", s.handleGetProjectLineage) mux.HandleFunc("GET /api/v1/projects/{id}/documents", s.handleListProjectWorkflowDocuments) + mux.HandleFunc("GET /api/v1/projects/{id}/notifications", s.handleListProjectNotifications) // 3. 对话协同服务 (标准 JSON 阻塞) mux.HandleFunc("POST /api/v1/chat", s.handleChat) diff --git a/internal/server/workflow_approval_hook.go b/internal/server/workflow_approval_hook.go new file mode 100644 index 00000000..a38db097 --- /dev/null +++ b/internal/server/workflow_approval_hook.go @@ -0,0 +1,45 @@ +package server + +import ( + "context" + + "chat-agent-system/internal/docgov" + "chat-agent-system/internal/notification" +) + +func (s *Server) registerWorkflowApprovalHook() { + if s == nil || s.svc == nil || s.svc.DocGov() == nil || s.instSvc == nil || !s.instSvc.Available() { + return + } + s.svc.DocGov().AddOnApprovedHook(s.onWorkflowDocumentApproved) +} + +func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) { + if doc == nil || doc.TaskKey == "" || doc.ProjectID <= 0 || s.instSvc == nil { + return + } + + result, err := s.instSvc.HandleDocumentApproved(ctx, doc.ProjectID, doc.TaskKey) + if err != nil || result == nil || !result.StageReady { + return + } + if s.notifySvc == nil || !s.notifySvc.Available() { + return + } + + project, err := s.pwSvc.GetProject(ctx, doc.ProjectID, 0) + if err != nil || project == nil || project.CreatorUserID <= 0 { + return + } + + _, _ = s.notifySvc.EmitStageReadyToAdvance(ctx, project.CreatorUserID, notification.StageReadyPayload{ + ProjectID: doc.ProjectID, + WorkflowInstanceID: result.InstanceID, + StageInstanceID: result.StageInstanceID, + StageKey: result.StageKey, + StageName: result.StageName, + ConfigRevision: result.ConfigRevision, + RequiredDocCount: result.RequiredDocCount, + ApprovedDocCount: result.ApprovedDocCount, + }) +}