package main import ( "context" "encoding/json" "errors" "net/http" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" ) type attentionRepository interface { AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error) CreateAttentionItem(context.Context, continuity.AttentionItem) (continuity.AttentionItem, error) } func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) { mux.HandleFunc("GET /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) { session, ok := creatorSessionForAttention(w, r, identityClient) if !ok { return } limit, err := parseListLimit(r.URL.Query().Get("limit")) if err != nil { http.Error(w, "invalid attention limit", http.StatusBadRequest) return } items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, limit) if err != nil { http.Error(w, "attention unavailable", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(continuity.AttentionListResult{Items: items}) }) mux.HandleFunc("GET /v1/creator/today", func(w http.ResponseWriter, r *http.Request) { session, ok := creatorSessionForAttention(w, r, identityClient) if !ok { return } items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, 50) if err != nil { http.Error(w, "today unavailable", http.StatusInternalServerError) return } result := continuity.TodayResult{Items: make([]continuity.TodayItem, 0, len(items))} for _, item := range items { result.Items = append(result.Items, continuity.TodayItem{ Title: item.Summary, WhenUnixMillis: item.DueAtUnixMillis, AttentionLevel: item.Level, AttentionState: item.State, HumanState: humanAttentionState(item.State), DeliveryState: humanDeliveryState(item), SourceDescription: humanAttentionSource(item.SourceKind), ResponseHint: humanResponseHint(item), }) } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(result) }) mux.HandleFunc("POST /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) { session, ok := creatorSessionForAttention(w, r, identityClient) if !ok { return } var request continuity.CreateAttentionRequest decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { http.Error(w, "invalid attention request", http.StatusBadRequest) return } if request.OwnerPrincipalID == "" { request.OwnerPrincipalID = session.PrincipalID } if request.OwnerPrincipalID != session.PrincipalID || request.ScopeID == "" || request.SourceKind == "" || request.Level == "" || request.Summary == "" || request.CreatedAtUnixMillis < 1 || request.DueAtUnixMillis < 1 { http.Error(w, "invalid attention request", http.StatusBadRequest) return } if request.SourceKind == continuity.AttentionSourceAgentDelegation && request.SourceExternalRef == "" { http.Error(w, "delegation attention requires source external ref", http.StatusBadRequest) return } item := continuity.AttentionItem{ ID: continuity.AttentionItemID(request.SourceExternalRef), OwnerPrincipalID: request.OwnerPrincipalID, ScopeID: request.ScopeID, SourceKind: request.SourceKind, SourceProspectiveID: request.SourceProspectiveID, SourceExternalRef: request.SourceExternalRef, Level: request.Level, State: continuity.AttentionPending, Summary: request.Summary, DueAtUnixMillis: request.DueAtUnixMillis, CreatedAtUnixMillis: request.CreatedAtUnixMillis, Revision: 1, } saved, err := repository.CreateAttentionItem(r.Context(), item) if err != nil { http.Error(w, "attention unavailable", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(saved) }) } func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) { session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization")) if errors.Is(err, errCreatorSessionDenied) { http.Error(w, "creator session denied", http.StatusUnauthorized) return identity.CreatorSession{}, false } if err != nil { http.Error(w, "identity service unavailable", http.StatusServiceUnavailable) return identity.CreatorSession{}, false } return session, true } func humanAttentionState(state continuity.AttentionState) string { switch state { case continuity.AttentionPending: return "ожидает внимания" case continuity.AttentionDelivering: return "доставляется" case continuity.AttentionDelivered: return "доставлено" case continuity.AttentionResolved: return "закрыто" case continuity.AttentionSuppressed: return "скрыто" case continuity.AttentionExpired: return "истекло" case continuity.AttentionFailed: return "не доставлено" default: return "состояние уточняется" } } func humanResponseHint(item continuity.AttentionItem) string { if item.SourceKind == continuity.AttentionSourceAgentDelegation { if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed { return "Можно ответить: «Продолжай» или «Нет, нужен другой вариант»." } if item.ResponseOutcome != "" { return "Ответ записан: " + string(item.ResponseOutcome) } return "" } if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed { return "Можно ответить: «Готово», «Отложи на 10 минут» или «Отмени»." } if item.ResponseOutcome != "" { return "Ответ записан: " + string(item.ResponseOutcome) } return "" } func humanDeliveryState(item continuity.AttentionItem) string { if item.State == continuity.AttentionDelivered { return "уведомление отправлено" } if item.State == continuity.AttentionDelivering { return "уведомление отправляется" } return "уведомление ещё не отправлено" } func humanAttentionSource(kind continuity.AttentionSourceKind) string { if kind == continuity.AttentionSourceProspectiveReminder { return "напоминание" } if kind == continuity.AttentionSourceAgentDelegation { return "задача coding-agent" } return "событие Домового" }