Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

292 lines
8.5 KiB
Go

package agent
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/tutti-os/tutti/services/tuttid/biz/agentprovider"
agentstatusservice "github.com/tutti-os/tutti/services/tuttid/service/agentstatus"
)
const (
ProviderAvailabilityAvailable = "available"
ProviderAvailabilityUnavailable = "unavailable"
ProviderAvailabilityUnknown = "unknown"
)
var ErrProviderUnavailable = errors.New("agent provider unavailable")
type ProviderUnavailableError struct {
Provider string
ReasonCode string
Message string
}
func (e *ProviderUnavailableError) Error() string {
if e == nil {
return ErrProviderUnavailable.Error()
}
message := strings.TrimSpace(e.Message)
if message == "" {
message = ErrProviderUnavailable.Error()
}
provider := strings.TrimSpace(e.Provider)
if provider == "" {
return message
}
return fmt.Sprintf("%s: %s", provider, message)
}
func (*ProviderUnavailableError) Unwrap() error {
return ErrProviderUnavailable
}
type ProviderAvailabilityCheck struct {
Name string
Passed bool
Detail string
}
type ProviderAvailabilityError struct {
Code string
Message string
}
type ProviderAvailability struct {
Provider string
Status string
ExecutablePath string
Checks []ProviderAvailabilityCheck
LastError *ProviderAvailabilityError
CapturedAt time.Time
}
type ProviderAvailabilityInput struct {
Provider string
}
type ProviderAvailabilityChecker interface {
ListProviderAvailability(context.Context, []string) ([]ProviderAvailability, error)
}
type ProviderAvailabilityInvalidator interface {
InvalidateProviderAvailability(string)
}
type AgentStatusProviderAvailabilityChecker struct {
Service AgentProviderStatusLister
}
type AgentProviderStatusLister interface {
List(context.Context, agentstatusservice.ListInput) (agentstatusservice.Snapshot, error)
}
func (c AgentStatusProviderAvailabilityChecker) InvalidateProviderAvailability(provider string) {
if invalidator, ok := c.Service.(interface{ Invalidate(string) }); ok {
invalidator.Invalidate(provider)
}
}
func (s *Service) invalidateProviderAvailability(provider string) {
s.providerAvailabilityCache.invalidate(provider)
if invalidator, ok := s.AvailabilityChecker.(ProviderAvailabilityInvalidator); ok {
invalidator.InvalidateProviderAvailability(provider)
}
}
// InvalidateProviderAvailabilityCache drops the agent service's derived
// availability snapshot when the status service changes its source data.
// It intentionally does not call back into the status service.
func (s *Service) InvalidateProviderAvailabilityCache(provider string) {
s.providerAvailabilityCache.invalidate(provider)
}
func (s *Service) ListProviderAvailability(ctx context.Context, input ProviderAvailabilityInput) ([]ProviderAvailability, error) {
providers, err := providerAvailabilityInputProviders(input)
if err != nil {
return nil, err
}
cacheKey := providerAvailabilityCacheKey(providers)
now := time.Now().UTC()
if cached, ok := s.providerAvailabilityCache.get(cacheKey, now, s.providerAvailabilityCacheTTL()); ok {
return cached, nil
}
checker := s.AvailabilityChecker
if checker == nil {
checker = AgentStatusProviderAvailabilityChecker{Service: agentstatusservice.Service{}}
}
availability, err := checker.ListProviderAvailability(ctx, providers)
if errors.Is(err, agentstatusservice.ErrInvalidProvider) {
return nil, ErrInvalidArgument
}
if err != nil {
return nil, err
}
s.providerAvailabilityCache.set(cacheKey, time.Now().UTC(), availability)
return cloneProviderAvailability(availability), nil
}
func providerTargetRefKind(providerTargetRef map[string]any) string {
kind, _ := providerTargetRef["kind"].(string)
return strings.TrimSpace(kind)
}
func providerAvailabilityInputProviders(input ProviderAvailabilityInput) ([]string, error) {
provider := strings.TrimSpace(input.Provider)
if provider == "" {
return nil, nil
}
normalized := agentprovider.Normalize(provider)
if normalized == "" {
return nil, ErrInvalidArgument
}
return []string{normalized}, nil
}
func (s *Service) providerAvailabilityCacheTTL() time.Duration {
if s.ProviderAvailabilityCacheTTL != 0 {
return s.ProviderAvailabilityCacheTTL
}
return defaultProviderAvailabilityCacheTTL
}
func (c AgentStatusProviderAvailabilityChecker) ListProviderAvailability(
ctx context.Context,
providers []string,
) ([]ProviderAvailability, error) {
service := c.Service
if service == nil {
service = agentstatusservice.Service{}
}
snapshot, err := service.List(ctx, agentstatusservice.ListInput{Providers: providers})
if err != nil {
return nil, err
}
result := make([]ProviderAvailability, 0, len(snapshot.Providers))
for _, status := range snapshot.Providers {
result = append(result, providerAvailabilityFromAgentStatus(snapshot.CapturedAt, status))
}
return result, nil
}
func providerAvailabilityFromAgentStatus(
capturedAt time.Time,
status agentstatusservice.ProviderStatus,
) ProviderAvailability {
checkedAt := capturedAt
if status.Availability.CheckedAt != nil {
checkedAt = status.Availability.CheckedAt.UTC()
}
result := ProviderAvailability{
Provider: strings.TrimSpace(status.Provider),
Status: providerAvailabilityStatusFromAgentStatus(status.Availability.Status),
ExecutablePath: strings.TrimSpace(status.CLI.BinaryPath),
Checks: providerAvailabilityChecksFromAgentStatus(status),
CapturedAt: checkedAt,
}
if result.Status != ProviderAvailabilityAvailable {
result.LastError = providerAvailabilityErrorFromAgentStatus(status)
}
return result
}
func providerAvailabilityStatusFromAgentStatus(status agentstatusservice.AvailabilityStatus) string {
switch status {
case agentstatusservice.AvailabilityReady:
return ProviderAvailabilityAvailable
case agentstatusservice.AvailabilityNotInstalled,
agentstatusservice.AvailabilityAuthRequired,
agentstatusservice.AvailabilityUnsupported:
return ProviderAvailabilityUnavailable
default:
return ProviderAvailabilityUnknown
}
}
func providerAvailabilityChecksFromAgentStatus(status agentstatusservice.ProviderStatus) []ProviderAvailabilityCheck {
if status.Availability.Status == agentstatusservice.AvailabilityUnsupported {
return []ProviderAvailabilityCheck{{
Name: "support",
Passed: false,
Detail: providerAvailabilityErrorMessage(status),
}}
}
return []ProviderAvailabilityCheck{
{
Name: "cli",
Passed: status.CLI.Installed,
Detail: firstNonEmptyString(status.CLI.BinaryPath, "CLI binary not found"),
},
{
Name: "adapter",
Passed: status.Adapter.Installed,
Detail: providerAvailabilityAdapterDetail(status),
},
{
Name: "auth",
Passed: status.Auth.Status == agentstatusservice.AuthAuthenticated,
Detail: providerAvailabilityAuthDetail(status.Auth),
},
}
}
func providerAvailabilityAdapterDetail(status agentstatusservice.ProviderStatus) string {
if strings.TrimSpace(status.Adapter.BinaryPath) != "" {
return strings.TrimSpace(status.Adapter.BinaryPath)
}
switch strings.TrimSpace(status.Availability.ReasonCode) {
case agentstatusservice.ReasonClaudeSDKSidecarUnavailable:
return "Claude SDK sidecar not found"
case agentstatusservice.ReasonManagedRuntimeUnavailable:
return "Managed Node runtime is unavailable"
default:
return "ACP adapter not found"
}
}
func providerAvailabilityAuthDetail(auth agentstatusservice.AuthInfo) string {
switch auth.Status {
case agentstatusservice.AuthAuthenticated:
return firstNonEmptyString(auth.AccountLabel, "authenticated")
case agentstatusservice.AuthRequired:
return "authentication required"
default:
return "authentication unknown"
}
}
func providerAvailabilityErrorFromAgentStatus(status agentstatusservice.ProviderStatus) *ProviderAvailabilityError {
code := strings.TrimSpace(status.Availability.ReasonCode)
if code == "" {
code = string(status.Availability.Status)
}
return &ProviderAvailabilityError{
Code: code,
Message: providerAvailabilityErrorMessage(status),
}
}
func providerAvailabilityErrorMessage(status agentstatusservice.ProviderStatus) string {
switch status.Availability.Status {
case agentstatusservice.AvailabilityNotInstalled:
if !status.CLI.Installed {
return "CLI binary not found"
}
if !status.Adapter.Installed {
return providerAvailabilityAdapterDetail(status)
}
return "provider is not installed"
case agentstatusservice.AvailabilityAuthRequired:
return "authentication required"
case agentstatusservice.AvailabilityUnsupported:
return "provider is temporarily unsupported"
case agentstatusservice.AvailabilityUnknown:
return "provider availability is unknown"
default:
return "provider is unavailable"
}
}