package batch import ( "encoding/json" "fmt" "strings" "sub2api-cn-relay-manager/internal/probe" "sub2api-cn-relay-manager/internal/store/sqlite" ) type ProjectionBadge struct { Kind string `json:"kind"` Tone string `json:"tone"` Label string `json:"label"` } type RunSummaryProjection struct { RunID string `json:"run_id"` State string `json:"state"` StateBadge ProjectionBadge `json:"state_badge"` Mode string `json:"mode"` AccessMode string `json:"access_mode"` TotalItems int `json:"total_items"` CompletedItems int `json:"completed_items"` ActiveItems int `json:"active_items"` DegradedItems int `json:"degraded_items"` BrokenItems int `json:"broken_items"` WarningItems int `json:"warning_items"` StartedAt string `json:"started_at"` FinishedAt string `json:"finished_at"` RecentWarnings []string `json:"recent_warnings"` } type ItemSummaryProjection struct { ItemID string `json:"item_id"` BaseURL string `json:"base_url"` ProviderID string `json:"provider_id"` APIKeyFingerprint string `json:"api_key_fingerprint"` RequestedModels []string `json:"requested_models"` CanonicalModelFamilies []string `json:"canonical_model_families"` ResolvedSmokeModel string `json:"resolved_smoke_model"` CurrentStage string `json:"current_stage"` ConfirmationStatus string `json:"confirmation_status"` AccessStatus string `json:"access_status"` MatchedAccountState string `json:"matched_account_state"` AccountResolution string `json:"account_resolution"` ProvisionReused bool `json:"provision_reused"` RetryCount int `json:"retry_count"` LastRetryAt string `json:"last_retry_at"` AdvisoryMessages []string `json:"advisory_messages"` LastErrorStage string `json:"last_error_stage"` LastError string `json:"last_error"` Badges []ProjectionBadge `json:"badges"` } type EventProjection struct { EventID string `json:"event_id"` EventType string `json:"event_type"` Stage string `json:"stage"` Attempt int `json:"attempt"` Message string `json:"message"` PayloadJSON string `json:"payload_json"` CreatedAt string `json:"created_at"` } type ItemDetailProjection struct { ItemSummaryProjection RawModels []string `json:"raw_models"` NormalizedModels []string `json:"normalized_models"` RecommendedModels []string `json:"recommended_models"` ReusedFromProviderID string `json:"reused_from_provider_id"` ReusedFromAccountID *int64 `json:"reused_from_account_id"` ChannelID *int64 `json:"channel_id"` AccountID *int64 `json:"account_id"` CapabilityProfile probe.CapabilityProfile `json:"capability_profile"` Events []EventProjection `json:"events"` } func ProjectRunSummary(run sqlite.ImportRun) RunSummaryProjection { view := RunSummaryProjection{ RunID: run.RunID, State: run.State, StateBadge: runStateBadge(run.State), Mode: run.Mode, AccessMode: run.AccessMode, TotalItems: run.TotalItems, CompletedItems: run.CompletedItems, ActiveItems: run.ActiveItems, DegradedItems: run.DegradedItems, BrokenItems: run.BrokenItems, WarningItems: run.WarningItems, StartedAt: run.StartedAt, FinishedAt: run.FinishedAt, RecentWarnings: []string{}, } if run.WarningItems > 0 { view.RecentWarnings = append(view.RecentWarnings, fmt.Sprintf("该批次包含 %d 条 advisory item,建议检查 capability profile 与 retry 轨迹", run.WarningItems)) } return view } func ProjectItemSummary(item sqlite.ImportRunItem) ItemSummaryProjection { view := ItemSummaryProjection{ ItemID: item.ItemID, BaseURL: item.BaseURL, ProviderID: item.ProviderID, APIKeyFingerprint: item.APIKeyFingerprint, RequestedModels: parseJSONStringArray(item.RequestedModelsJSON), CanonicalModelFamilies: parseJSONStringArray(item.CanonicalFamiliesJSON), ResolvedSmokeModel: item.ResolvedSmokeModel, CurrentStage: item.CurrentStage, ConfirmationStatus: item.ConfirmationStatus, AccessStatus: item.AccessStatus, MatchedAccountState: item.MatchedAccountState, AccountResolution: item.AccountResolution, ProvisionReused: item.ProvisionReused, RetryCount: item.RetryCount, LastRetryAt: item.LastRetryAt, AdvisoryMessages: mapAdvisoryMessages(parseJSONStringArray(item.AdvisoryMessagesJSON)), LastErrorStage: item.LastErrorStage, LastError: item.LastError, Badges: []ProjectionBadge{}, } if item.ProvisionReused { view.Badges = append(view.Badges, ProjectionBadge{Kind: "reused", Tone: "cyan", Label: "reused"}) } if badge, ok := matchedAccountStateBadge(item.MatchedAccountState); ok { view.Badges = append(view.Badges, badge) } if badge, ok := accountResolutionBadge(item.AccountResolution); ok { view.Badges = append(view.Badges, badge) } return view } func ProjectItemDetail(item sqlite.ImportRunItem, events []sqlite.ImportRunItemEvent) (ItemDetailProjection, error) { view := ItemDetailProjection{ ItemSummaryProjection: ProjectItemSummary(item), RawModels: parseJSONStringArray(item.RawModelsJSON), NormalizedModels: parseJSONStringArray(item.NormalizedModelsJSON), RecommendedModels: parseJSONStringArray(item.RecommendedModelsJSON), ReusedFromProviderID: item.ReusedFromProviderID, ReusedFromAccountID: item.ReusedFromAccountID, ChannelID: item.ChannelID, AccountID: item.AccountID, CapabilityProfile: probe.CapabilityProfile{}, Events: make([]EventProjection, 0, len(events)), } if payload := strings.TrimSpace(item.CapabilityProfileJSON); payload != "" { if err := json.Unmarshal([]byte(payload), &view.CapabilityProfile); err != nil { return ItemDetailProjection{}, fmt.Errorf("decode capability profile: %w", err) } } for _, event := range events { view.Events = append(view.Events, EventProjection{ EventID: event.EventID, EventType: event.EventType, Stage: event.Stage, Attempt: event.Attempt, Message: event.Message, PayloadJSON: event.PayloadJSON, CreatedAt: event.CreatedAt, }) } return view, nil } func parseJSONStringArray(raw string) []string { values := []string{} if err := json.Unmarshal([]byte(defaultJSONString(raw, "[]")), &values); err != nil { return []string{} } return values } func mapAdvisoryMessages(messages []string) []string { mapped := make([]string, 0, len(messages)) for _, message := range messages { switch strings.TrimSpace(message) { case "responses_unsupported_but_chat_ok": mapped = append(mapped, "该上游不支持 /v1/responses,系统已自动回退到 /v1/chat/completions") case "initial_probe_race_expected": mapped = append(mapped, "账号创建后宿主异步探测尚未稳定,首次 /test 已按 advisory 处理") case "gateway_warmup_retry_succeeded": mapped = append(mapped, "初次调度出现 no available accounts,短暂重试后已恢复") case "provision_reused": mapped = append(mapped, "已检测到同 URL + 同模型家族 + 健康账号,系统直接复用已有 provider") case "patch_only_new_aliases": mapped = append(mapped, "模型属于已覆盖家族,仅补充别名映射与定价,不重复创建资源") case "duplicate_active_account": mapped = append(mapped, "该账号已存在且处于启用状态,本次未重复创建,直接复用") case "deprecated_account_reactivated": mapped = append(mapped, "该账号此前处于弃用/停用状态,本次已快速启用并重新确认") default: if trimmed := strings.TrimSpace(message); trimmed != "" { mapped = append(mapped, trimmed) } } } return mapped } func runStateBadge(state string) ProjectionBadge { switch strings.TrimSpace(state) { case string(RunStateRunning): return ProjectionBadge{Kind: "state", Tone: "blue", Label: "running"} case string(RunStateCompleted): return ProjectionBadge{Kind: "state", Tone: "green", Label: "completed"} case string(RunStateCompletedWithWarnings): return ProjectionBadge{Kind: "state", Tone: "yellow", Label: "warning"} case string(RunStateFailed): return ProjectionBadge{Kind: "state", Tone: "red", Label: "failed"} case string(RunStateCancelled): return ProjectionBadge{Kind: "state", Tone: "gray", Label: "cancelled"} default: return ProjectionBadge{Kind: "state", Tone: "gray", Label: strings.TrimSpace(state)} } } func matchedAccountStateBadge(state string) (ProjectionBadge, bool) { switch strings.TrimSpace(state) { case string(MatchedAccountStateActive): return ProjectionBadge{Kind: "matched_account_state", Tone: "green", Label: "已启用"}, true case string(MatchedAccountStateDisabled): return ProjectionBadge{Kind: "matched_account_state", Tone: "gray", Label: "已停用"}, true case string(MatchedAccountStateDeprecated): return ProjectionBadge{Kind: "matched_account_state", Tone: "yellow", Label: "已弃用"}, true case string(MatchedAccountStateBroken): return ProjectionBadge{Kind: "matched_account_state", Tone: "red", Label: "已损坏"}, true default: return ProjectionBadge{}, false } } func accountResolutionBadge(resolution string) (ProjectionBadge, bool) { switch strings.TrimSpace(resolution) { case string(AccountResolutionCreated): return ProjectionBadge{Kind: "account_resolution", Tone: "blue", Label: "新建"}, true case string(AccountResolutionReused): return ProjectionBadge{Kind: "account_resolution", Tone: "cyan", Label: "复用"}, true case string(AccountResolutionReactivated): return ProjectionBadge{Kind: "account_resolution", Tone: "green", Label: "已快速启用"}, true case string(AccountResolutionReplaced): return ProjectionBadge{Kind: "account_resolution", Tone: "red", Label: "已替换"}, true default: return ProjectionBadge{}, false } }