Files
Agent-System-Backend/internal/cardsvc/account_checked.go
T

250 lines
8.3 KiB
Go

package cardsvc
import (
"agent-system-backend/internal/store"
core "agent-system-core"
"errors"
)
func (s *Service) requireAccountCheckedEmitter() error {
if s.emit != nil && s.accountCheckedEmit == nil {
return ErrGuardedEmitterRequired
}
return nil
}
func (s *Service) ResolveByRefAccountChecked(actor, ref, actionID, answer string, allowImplicitDefault bool) error {
if err := s.requireAccountCheckedEmitter(); err != nil {
return err
}
id, err := s.st.CardIDByRef(actor, ref)
if errors.Is(err, store.ErrNotFound) {
return nil
}
if err != nil {
return err
}
card, version, err := s.st.GetCardVersion(actor, id)
if err != nil {
return err
}
if card.Status.Terminal() {
return nil
}
card.Status = core.CardResolved
recordAnswer(&card, actionID, answer)
if err := s.UpdateVersionedAccountChecked(actor, card, version, allowImplicitDefault); errors.Is(err, store.ErrConflict) {
return nil
} else if err != nil {
return err
}
return s.emitCardAccountChecked(actor, card, core.EvCardResolved,
core.CardResolvedPayload{CardID: card.ID, ActionID: actionID, Answer: answer})
}
// AppendAccountChecked preserves stream-card throttling while fencing its
// durable body update against account disable.
func (s *Service) AppendAccountChecked(actor, id, text string, allowImplicitDefault bool) error {
if err := s.requireAccountCheckedEmitter(); err != nil {
return err
}
card, version, err := s.st.GetCardVersion(actor, id)
if err != nil {
return err
}
if card.Task != nil {
return ErrNoAction
}
if text != "" {
if n := len(card.Body); n > 0 && card.Body[n-1].Type == core.UIMarkdown {
card.Body[n-1].Markdown += text
} else {
card.Body = append(card.Body, core.UINode{Type: core.UIMarkdown, Markdown: text})
}
}
card.UpdatedAt = s.now().UTC()
if err := s.st.SaveCardVersionedAccountChecked(actor, &card, "", version, allowImplicitDefault); err != nil {
return err
}
if card.ConversationID == "" && card.ProjectID == "" {
return nil
}
s.emitMu.Lock()
last, seen := s.lastEmit[id]
emit := !seen || s.now().Sub(last) >= cardAppendWindow
if emit {
s.lastEmit[id] = s.now()
}
s.emitMu.Unlock()
if !emit {
return nil
}
return s.emitCardAccountChecked(actor, card, core.EvCardUpdated, core.CardPayload{Card: card})
}
func (s *Service) emitCardAccountChecked(actor string, card core.ActivityCard, typ core.StreamEventType, payload any) error {
if s.accountCheckedEmit == nil || (card.ConversationID == "" && card.ProjectID == "") {
return nil
}
if card.ConversationID != "" {
if err := s.accountCheckedEmit(actor, card.ConversationID, card.SourceRunID, typ, payload); err != nil {
return err
}
if !s.MirrorConvCards {
return nil
}
}
if card.ProjectID != "" {
return s.accountCheckedEmit(actor, string(core.ProjectChannelKey(card.ProjectID)), card.SourceRunID, typ, payload)
}
return nil
}
// CreateAccountChecked creates a card under the actor's final Store fence.
func (s *Service) CreateAccountChecked(actor string, card core.ActivityCard, ref string, allowImplicitDefault bool) (core.ActivityCard, error) {
if err := s.requireAccountCheckedEmitter(); err != nil {
return card, err
}
if card.Task != nil {
return card, store.ErrConflict
}
if card.ID != "" {
if existing, err := s.st.GetCard(actor, card.ID); err == nil && existing.Task != nil {
return card, store.ErrConflict
}
}
if card.ID == "" {
card.ID = store.NewID("card")
}
now := s.now().UTC()
card.CreatedAt, card.UpdatedAt = now, now
if card.Status == "" {
card.Status = core.CardActive
}
if err := s.st.SaveCardAccountChecked(actor, &card, ref, allowImplicitDefault); err != nil {
return card, err
}
return card, s.emitCardAccountChecked(actor, card, core.EvCardCreated, core.CardPayload{Card: card})
}
func (s *Service) updateVersionedAccountChecked(actor string, card core.ActivityCard, expectedVersion int, allowImplicitDefault bool) (core.ActivityCard, error) {
if err := s.requireAccountCheckedEmitter(); err != nil {
return card, err
}
if card.Task == nil {
if current, err := s.st.GetCard(actor, card.ID); err == nil && current.Task != nil {
return card, store.ErrConflict
}
}
if card.Task != nil {
// Task cards keep their revision-checked projection and its in-transaction
// card events; the account fence covers that same transaction.
if err := s.commitProjection(store.TaskProjectionWrite{Owner: actor, Card: card, ExpectedCardVersion: expectedVersion,
ExpectedTaskRevision: card.Task.Revision, Channels: s.taskChannels(card), AccountActor: actor, AllowImplicitDefault: allowImplicitDefault}); err != nil {
return card, err
}
return s.Get(actor, card.ID)
}
card.UpdatedAt = s.now().UTC()
if err := s.st.SaveCardVersionedAccountChecked(actor, &card, "", expectedVersion, allowImplicitDefault); err != nil {
return card, err
}
return card, s.emitCardAccountChecked(actor, card, core.EvCardUpdated, core.CardPayload{Card: card})
}
func (s *Service) UpdateVersionedAccountChecked(actor string, card core.ActivityCard, expectedVersion int, allowImplicitDefault bool) error {
_, err := s.updateVersionedAccountChecked(actor, card, expectedVersion, allowImplicitDefault)
return err
}
func (s *Service) SetPinnedAccountChecked(actor, id string, pinned, allowImplicitDefault bool) (core.ActivityCard, error) {
card, version, err := s.st.GetCardVersion(actor, id)
if err != nil {
return card, err
}
card.Pinned = pinned
card, err = s.updateVersionedAccountChecked(actor, card, version, allowImplicitDefault)
if err != nil {
return card, err
}
return card, nil
}
func (s *Service) SetStatusAccountChecked(actor, id string, status core.CardStatus, tone, note string, allowImplicitDefault bool) (core.ActivityCard, error) {
card, version, err := s.st.GetCardVersion(actor, id)
if err != nil {
return card, err
}
card.Status = status
if tone != "" {
card.Tone = tone
}
card, err = s.updateVersionedAccountChecked(actor, card, version, allowImplicitDefault)
if err != nil {
return card, err
}
if card.Task != nil {
return card, nil
}
return card, s.emitCardAccountChecked(actor, card, core.EvCardStatus,
core.CardStatusPayload{CardID: card.ID, Status: status, Tone: card.Tone, Note: note})
}
func (s *Service) DismissAccountChecked(actor, id string, allowImplicitDefault bool) (core.ActivityCard, error) {
return s.SetStatusAccountChecked(actor, id, core.CardDismissed, "", "dismissed by user", allowImplicitDefault)
}
func (s *Service) ActionAccountChecked(actor, id, actionID, freeText string, allowImplicitDefault bool) (core.ActivityCard, string, error) {
card, version, err := s.st.GetCardVersion(actor, id)
if err != nil {
return card, "", err
}
if card.Status != core.CardActionRequired {
return card, "", ErrNotAwaiting
}
label, found := actionID, false
for _, action := range card.Actions {
if action.ID == actionID {
label, found = action.Label, true
break
}
}
if !found {
if actionID == "" && freeText != "" {
label = freeText
} else {
return card, "", ErrNoAction
}
}
card.Status = core.CardResolved
recordAnswer(&card, actionID, freeText)
card, err = s.updateVersionedAccountChecked(actor, card, version, allowImplicitDefault)
if err != nil {
return card, "", err
}
err = s.emitCardAccountChecked(actor, card, core.EvCardResolved,
core.CardResolvedPayload{CardID: card.ID, ActionID: actionID, Answer: freeText})
return card, label, err
}
// ToProjectAccountChecked performs the copy and dismissal in one transaction.
// Events are published after commit through the actor-aware event sink.
func (s *Service) ToProjectAccountChecked(actor, id string, allowImplicitDefault bool) (core.ActivityCard, error) {
if err := s.requireAccountCheckedEmitter(); err != nil {
return core.ActivityCard{}, err
}
archived, origin, created, err := s.st.ArchiveCardToProjectAccountChecked(actor, id, s.now().UTC(), allowImplicitDefault)
if err != nil || !created {
return archived, err
}
if err := s.emitCardAccountChecked(actor, archived, core.EvCardCreated, core.CardPayload{Card: archived}); err != nil {
return archived, err
}
if err := s.emitCardAccountChecked(actor, origin, core.EvCardUpdated, core.CardPayload{Card: origin}); err != nil {
return archived, err
}
err = s.emitCardAccountChecked(actor, origin, core.EvCardStatus,
core.CardStatusPayload{CardID: origin.ID, Status: core.CardDismissed, Tone: origin.Tone, Note: "已留档到项目"})
return archived, err
}