Files
agent-pet-companion/apps/macos/Sources/AgentPetCompanionCore/AppModels.swift
T

1175 lines
38 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
public enum NavigationSection: String, CaseIterable, Identifiable, Codable, Hashable, Sendable {
case studio
case behavior
case connections
public var id: String { rawValue }
public var title: String {
switch self {
case .studio: "宠物 Studio"
case .behavior: "启用与行为"
case .connections: "Agent 连接"
}
}
public var subtitle: String {
switch self {
case .studio: "Pet Studio"
case .behavior: "Enable & Behavior"
case .connections: "Agent Connections"
}
}
public var systemImage: String {
switch self {
case .studio: "sparkles"
case .behavior: "switch.2"
case .connections: "cable.connector"
}
}
}
public enum StudioTab: String, CaseIterable, Identifiable, Sendable {
case new
case library
public var id: String { rawValue }
public var title: String {
switch self {
case .new: "新建"
case .library: "宠物库"
}
}
}
public enum StylePreset: String, CaseIterable, Identifiable, Codable, Sendable {
case realistic = "写实"
case semiRealistic = "半写实"
case modern = "现代"
case pixel = "像素"
case anime = "动漫"
case unspecified = "不指定"
public var id: String { rawValue }
}
public enum PetStudioDefaults {
public static let descriptionText = ""
}
public enum QualityLevel: String, CaseIterable, Identifiable, Codable, Sendable {
case standard
case high
case ultra
case original
public var id: String { rawValue }
public var title: String {
switch self {
case .standard: "标清"
case .high: "高清"
case .ultra: "超清"
case .original: "原画"
}
}
public var detail: String {
let size = renderSize
switch self {
case .high:
return "\(size.width)×\(size.height) · 推荐"
default:
return "\(size.width)×\(size.height)"
}
}
public var renderSize: RenderSize {
switch self {
case .standard: RenderSize(width: 192, height: 208)
case .high: RenderSize(width: 384, height: 416)
case .ultra: RenderSize(width: 768, height: 832)
case .original: RenderSize(width: 1536, height: 1664)
}
}
}
public struct RenderSize: Codable, Hashable, Sendable {
public var width: Int
public var height: Int
public init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
public enum FpsProfile: String, CaseIterable, Identifiable, Codable, Sendable {
case standard
case smooth
public var id: String { rawValue }
public var title: String {
switch self {
case .standard: "标准动效"
case .smooth: "流畅动效"
}
}
public var fps: Int {
switch self {
case .standard: 12
case .smooth: 20
}
}
}
public enum AgentSource: String, CaseIterable, Identifiable, Codable, Sendable {
case codex
case claudeCode = "claude_code"
case pi
case opencode
public var id: String { rawValue }
public var title: String {
switch self {
case .codex: "Codex"
case .claudeCode: "Claude Code"
case .pi: "Pi Coding Agent"
case .opencode: "OpenCode"
}
}
public var shortTitle: String {
switch self {
case .codex: "Codex"
case .claudeCode: "Claude"
case .pi: "Pi"
case .opencode: "OpenCode"
}
}
}
public enum AgentEventKind: String, CaseIterable, Identifiable, Codable, Sendable {
case start
case tool
case waiting
case review
case done
case failed
public var id: String { rawValue }
public var title: String {
switch self {
case .start: "开始处理"
case .tool: "执行工具"
case .waiting: "等待确认"
case .review: "待查看"
case .done: "完成"
case .failed: "失败"
}
}
public var petState: String { rawValue }
}
public struct BehaviorSettings: Codable, Equatable, Sendable {
public var enabled: Bool
public var statusBubble: Bool
public var clickMenu: Bool
public var mousePassthrough: Bool
public var autoHide: Bool
public var sessionMessageTimeoutMinutes: Int
public var fpsProfile: FpsProfile
public var sources: [AgentSource: Bool]
public var events: [AgentEventKind: Bool]
public init(
enabled: Bool = true,
statusBubble: Bool = true,
clickMenu: Bool = true,
mousePassthrough: Bool = true,
autoHide: Bool = false,
sessionMessageTimeoutMinutes: Int = 15,
fpsProfile: FpsProfile = .standard,
sources: [AgentSource: Bool] = Dictionary(uniqueKeysWithValues: AgentSource.allCases.map { ($0, true) }),
events: [AgentEventKind: Bool] = Dictionary(uniqueKeysWithValues: AgentEventKind.allCases.map { ($0, true) })
) {
self.enabled = enabled
self.statusBubble = statusBubble
self.clickMenu = clickMenu
self.mousePassthrough = mousePassthrough
self.autoHide = autoHide
self.sessionMessageTimeoutMinutes = sessionMessageTimeoutMinutes
self.fpsProfile = fpsProfile
self.sources = sources
self.events = events
}
enum CodingKeys: String, CodingKey {
case enabled
case statusBubble = "status_bubble"
case clickMenu = "click_menu"
case mousePassthrough = "mouse_passthrough"
case autoHide = "auto_hide"
case sessionMessageTimeoutMinutes = "session_message_timeout_minutes"
case fpsProfile = "fps_profile"
case sources
case events
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let defaults = BehaviorSettings()
enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? defaults.enabled
statusBubble = try container.decodeIfPresent(Bool.self, forKey: .statusBubble) ?? defaults.statusBubble
clickMenu = try container.decodeIfPresent(Bool.self, forKey: .clickMenu) ?? defaults.clickMenu
mousePassthrough = try container.decodeIfPresent(Bool.self, forKey: .mousePassthrough) ?? defaults.mousePassthrough
autoHide = try container.decodeIfPresent(Bool.self, forKey: .autoHide) ?? defaults.autoHide
sessionMessageTimeoutMinutes = try container.decodeIfPresent(
Int.self,
forKey: .sessionMessageTimeoutMinutes
) ?? defaults.sessionMessageTimeoutMinutes
fpsProfile = try container.decodeIfPresent(FpsProfile.self, forKey: .fpsProfile) ?? defaults.fpsProfile
let rawSources = try container.decodeIfPresent([String: Bool].self, forKey: .sources) ?? [:]
sources = Dictionary(uniqueKeysWithValues: AgentSource.allCases.map { source in
(source, rawSources[source.rawValue] ?? defaults.sources[source, default: true])
})
let rawEvents = try container.decodeIfPresent([String: Bool].self, forKey: .events) ?? [:]
events = Dictionary(uniqueKeysWithValues: AgentEventKind.allCases.map { event in
(event, rawEvents[event.rawValue] ?? defaults.events[event, default: true])
})
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(enabled, forKey: .enabled)
try container.encode(statusBubble, forKey: .statusBubble)
try container.encode(clickMenu, forKey: .clickMenu)
try container.encode(mousePassthrough, forKey: .mousePassthrough)
try container.encode(autoHide, forKey: .autoHide)
try container.encode(sessionMessageTimeoutMinutes, forKey: .sessionMessageTimeoutMinutes)
try container.encode(fpsProfile, forKey: .fpsProfile)
try container.encode(
Dictionary(uniqueKeysWithValues: sources.map { ($0.key.rawValue, $0.value) }),
forKey: .sources
)
try container.encode(
Dictionary(uniqueKeysWithValues: events.map { ($0.key.rawValue, $0.value) }),
forKey: .events
)
}
public func showsStatusBubble(hasActiveEvent: Bool, dismissed: Bool) -> Bool {
enabled && statusBubble && !dismissed && (!autoHide || hasActiveEvent)
}
}
public struct BehaviorSettingsPatch: Codable, Equatable, Sendable {
public var enabled: Bool?
public var statusBubble: Bool?
public var clickMenu: Bool?
public var mousePassthrough: Bool?
public var autoHide: Bool?
public var sessionMessageTimeoutMinutes: Int?
public var fpsProfile: FpsProfile?
public var sources: [AgentSource: Bool]?
public var events: [AgentEventKind: Bool]?
public init(from previous: BehaviorSettings, to next: BehaviorSettings) {
enabled = previous.enabled == next.enabled ? nil : next.enabled
statusBubble = previous.statusBubble == next.statusBubble ? nil : next.statusBubble
clickMenu = previous.clickMenu == next.clickMenu ? nil : next.clickMenu
mousePassthrough = previous.mousePassthrough == next.mousePassthrough
? nil
: next.mousePassthrough
autoHide = previous.autoHide == next.autoHide ? nil : next.autoHide
sessionMessageTimeoutMinutes = previous.sessionMessageTimeoutMinutes == next.sessionMessageTimeoutMinutes
? nil
: next.sessionMessageTimeoutMinutes
fpsProfile = previous.fpsProfile == next.fpsProfile ? nil : next.fpsProfile
let changedSources = next.sources.filter { previous.sources[$0.key] != $0.value }
sources = changedSources.isEmpty ? nil : changedSources
let changedEvents = next.events.filter { previous.events[$0.key] != $0.value }
events = changedEvents.isEmpty ? nil : changedEvents
}
public var isEmpty: Bool {
enabled == nil
&& statusBubble == nil
&& clickMenu == nil
&& mousePassthrough == nil
&& autoHide == nil
&& sessionMessageTimeoutMinutes == nil
&& fpsProfile == nil
&& sources?.isEmpty != false
&& events?.isEmpty != false
}
enum CodingKeys: String, CodingKey {
case enabled
case statusBubble = "status_bubble"
case clickMenu = "click_menu"
case mousePassthrough = "mouse_passthrough"
case autoHide = "auto_hide"
case sessionMessageTimeoutMinutes = "session_message_timeout_minutes"
case fpsProfile = "fps_profile"
case sources
case events
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled)
statusBubble = try container.decodeIfPresent(Bool.self, forKey: .statusBubble)
clickMenu = try container.decodeIfPresent(Bool.self, forKey: .clickMenu)
mousePassthrough = try container.decodeIfPresent(Bool.self, forKey: .mousePassthrough)
autoHide = try container.decodeIfPresent(Bool.self, forKey: .autoHide)
sessionMessageTimeoutMinutes = try container.decodeIfPresent(
Int.self,
forKey: .sessionMessageTimeoutMinutes
)
fpsProfile = try container.decodeIfPresent(FpsProfile.self, forKey: .fpsProfile)
let rawSources = try container.decodeIfPresent([String: Bool].self, forKey: .sources)
sources = rawSources.map { values in
Dictionary(uniqueKeysWithValues: values.compactMap { key, value in
AgentSource(rawValue: key).map { ($0, value) }
})
}
let rawEvents = try container.decodeIfPresent([String: Bool].self, forKey: .events)
events = rawEvents.map { values in
Dictionary(uniqueKeysWithValues: values.compactMap { key, value in
AgentEventKind(rawValue: key).map { ($0, value) }
})
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(enabled, forKey: .enabled)
try container.encodeIfPresent(statusBubble, forKey: .statusBubble)
try container.encodeIfPresent(clickMenu, forKey: .clickMenu)
try container.encodeIfPresent(mousePassthrough, forKey: .mousePassthrough)
try container.encodeIfPresent(autoHide, forKey: .autoHide)
try container.encodeIfPresent(
sessionMessageTimeoutMinutes,
forKey: .sessionMessageTimeoutMinutes
)
try container.encodeIfPresent(fpsProfile, forKey: .fpsProfile)
if let sources {
try container.encode(
Dictionary(uniqueKeysWithValues: sources.map { ($0.key.rawValue, $0.value) }),
forKey: .sources
)
}
if let events {
try container.encode(
Dictionary(uniqueKeysWithValues: events.map { ($0.key.rawValue, $0.value) }),
forKey: .events
)
}
}
}
public struct VersionedBehaviorSettings: Codable, Equatable, Sendable {
public var behavior: BehaviorSettings
public var revision: String
}
public struct PetSummary: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var name: String
public var style: String
public var quality: QualityLevel
public var renderSize: RenderSize
public var petpackPath: String
public var coverPath: String
public var origin: PetOrigin
public var generator: String?
public var provenance: String?
public var active: Bool
public var createdAt: String
public init(
id: String,
name: String,
style: String,
quality: QualityLevel,
renderSize: RenderSize,
petpackPath: String,
coverPath: String,
origin: PetOrigin = .externalImport,
generator: String? = nil,
provenance: String? = nil,
active: Bool,
createdAt: String
) {
self.id = id
self.name = name
self.style = style
self.quality = quality
self.renderSize = renderSize
self.petpackPath = petpackPath
self.coverPath = coverPath
self.origin = origin
self.generator = generator
self.provenance = provenance
self.active = active
self.createdAt = createdAt
}
enum CodingKeys: String, CodingKey {
case id
case name
case style
case quality
case renderSize = "render_size"
case petpackPath = "petpack_path"
case coverPath = "cover_path"
case origin
case generator
case provenance
case active
case createdAt = "created_at"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
style = try container.decode(String.self, forKey: .style)
quality = try container.decode(QualityLevel.self, forKey: .quality)
renderSize = try container.decode(RenderSize.self, forKey: .renderSize)
petpackPath = try container.decode(String.self, forKey: .petpackPath)
coverPath = try container.decode(String.self, forKey: .coverPath)
origin = try container.decodeIfPresent(PetOrigin.self, forKey: .origin) ?? .externalImport
generator = try container.decodeIfPresent(String.self, forKey: .generator)
provenance = try container.decodeIfPresent(String.self, forKey: .provenance)
active = try container.decode(Bool.self, forKey: .active)
createdAt = try container.decode(String.self, forKey: .createdAt)
}
public var generationSourceTitle: String {
switch origin {
case .verifiedSkillSource:
"已验证 Skill 来源"
case .generatedByPetcoreJob:
provenance == "skill-full-source" ? "App 内生成" : "本地动画预览"
case .externalImport:
"外部导入"
}
}
public var generationSourceDetail: String {
let claimed = [generator, provenance].compactMap { $0 }.joined(separator: " · ")
switch origin {
case .verifiedSkillSource:
return claimed.isEmpty ? "已通过 App Server Skill source 校验" : "已验证 · \(claimed)"
case .generatedByPetcoreJob:
if provenance == "deterministic_preview" || provenance == "local_form" {
return claimed.isEmpty ? "确定性预览,不代表 AI 图像生成" : "确定性预览 · \(claimed)"
}
if provenance == "codex_app_server_brief" {
return claimed.isEmpty ? "AI brief + 本地预览渲染" : "AI brief + 本地预览 · \(claimed)"
}
return claimed.isEmpty ? "由本 App generation job 写入" : "App job · \(claimed)"
case .externalImport:
return claimed.isEmpty ? "外部 .petpack 未记录包内声明" : "外部导入 · 包内声明:\(claimed)"
}
}
}
public struct PetAssetWarning: Codable, Hashable, Sendable {
public var petId: String
public var code: String
public var fingerprint: String
public var message: String
public init(petId: String, code: String, fingerprint: String, message: String) {
self.petId = petId
self.code = code
self.fingerprint = fingerprint
self.message = message
}
enum CodingKeys: String, CodingKey {
case petId = "pet_id"
case code
case fingerprint
case message
}
}
public struct PetAssetWarningIndex: Equatable, Sendable {
private var warningsByPetID: [String: PetAssetWarning]
public init(_ warnings: [PetAssetWarning] = []) {
warningsByPetID = Dictionary(warnings.map { ($0.petId, $0) }, uniquingKeysWith: { _, latest in latest })
}
public subscript(petID: String) -> PetAssetWarning? {
warningsByPetID[petID]
}
}
public enum PetOrigin: String, Codable, Hashable, Sendable {
case externalImport = "external_import"
case generatedByPetcoreJob = "generated_by_petcore_job"
case verifiedSkillSource = "verified_skill_source"
}
public struct OverlayPlacement: Codable, Hashable, Sendable {
public var x: Double
public var y: Double
public var scale: Double
public var displayId: String
public init(x: Double = 0, y: Double = 0, scale: Double = 0.72, displayId: String = "main") {
self.x = x
self.y = y
self.scale = scale
self.displayId = displayId
}
enum CodingKeys: String, CodingKey {
case x
case y
case scale
case displayId = "display_id"
}
}
public struct AgentEventPayload: Codable, Hashable, Sendable {
public var schemaVersion: String?
public var externalEventID: String?
public var sourceEvent: String?
public var toolName: String?
public var outcome: String?
public var diagnostic: Bool?
public var turnID: String?
public var sessionActive: Bool?
public var messageRole: String?
public var messageContent: String?
public var activityKind: String?
public var activityContent: String?
public var interactionKind: String?
public var projectLabel: String?
public var sessionTitle: String?
public var sessionOpen: Bool?
public var sessionSurface: String?
public var terminalApp: String?
public var sessionOpenURL: String?
enum CodingKeys: String, CodingKey {
case schemaVersion = "schema_version"
case externalEventID = "external_event_id"
case sourceEvent = "source_event"
case toolName = "tool_name"
case outcome
case diagnostic
case turnID = "turn_id"
case sessionActive = "session_active"
case messageRole = "message_role"
case messageContent = "message_content"
case activityKind = "activity_kind"
case activityContent = "activity_content"
case interactionKind = "interaction_kind"
case projectLabel = "project_label"
case sessionTitle = "session_title"
case sessionOpen = "session_open"
case sessionSurface = "session_surface"
case terminalApp = "terminal_app"
case sessionOpenURL = "session_open_url"
}
}
public struct AgentSessionNavigation: Codable, Hashable, Sendable {
public var sessionOpen: Bool?
public var surface: String?
public var terminalApp: String?
public var openURL: String?
public init(
sessionOpen: Bool? = nil,
surface: String? = nil,
terminalApp: String? = nil,
openURL: String? = nil
) {
self.sessionOpen = sessionOpen
self.surface = surface
self.terminalApp = terminalApp
self.openURL = openURL
}
public var explicitlyClosed: Bool { sessionOpen == false }
}
public struct AgentEvent: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var source: AgentSource
public var sessionID: String?
public var eventType: AgentEventKind
public var title: String
public var detail: String?
public var payloadJSON: AgentEventPayload?
public var createdAt: String
public var messageContent: String? {
payloadJSON?.messageContent?.trimmingCharacters(in: .whitespacesAndNewlines)
}
public var sessionNavigation: AgentSessionNavigation {
AgentSessionNavigation(
sessionOpen: payloadJSON?.sessionOpen,
surface: payloadJSON?.sessionSurface,
terminalApp: payloadJSON?.terminalApp,
openURL: payloadJSON?.sessionOpenURL
)
}
public init(
id: String,
source: AgentSource,
sessionID: String? = nil,
eventType: AgentEventKind,
title: String,
detail: String? = nil,
payloadJSON: AgentEventPayload? = nil,
createdAt: String
) {
self.id = id
self.source = source
self.sessionID = sessionID
self.eventType = eventType
self.title = title
self.detail = detail
self.payloadJSON = payloadJSON
self.createdAt = createdAt
}
enum CodingKeys: String, CodingKey {
case id
case source
case sessionID = "session_id"
case eventType = "event_type"
case title
case detail
case payloadJSON = "payload_json"
case createdAt = "created_at"
}
}
public struct ActiveAgentState: Codable, Equatable, Sendable {
public var state: String
public var officialStatus: String?
public var source: AgentSource
public var sessionID: String?
public var sessionActive: Bool?
public var sourceSessionSequence: UInt64
public var priority: UInt16
public var leaseSeconds: Int?
public var expiresAt: String?
public var sessionActivatedAt: String?
public var event: AgentEvent
public var latestMessage: AgentEvent?
public var latestUserMessage: AgentEvent?
public var sessionTitle: String?
public var sessionMessage: AgentSessionDisplayMessage?
public var sessionUserMessage: AgentSessionDisplayMessage?
public var sessionActivity: AgentSessionActivity?
enum CodingKeys: String, CodingKey {
case state
case officialStatus = "official_status"
case source
case sessionID = "session_id"
case sessionActive = "session_active"
case sourceSessionSequence = "source_session_sequence"
case priority
case leaseSeconds = "lease_seconds"
case expiresAt = "expires_at"
case sessionActivatedAt = "session_activated_at"
case event
case latestMessage = "latest_message"
case latestUserMessage = "latest_user_message"
case sessionTitle = "session_title"
case sessionMessage = "session_message"
case sessionUserMessage = "session_user_message"
case sessionActivity = "session_activity"
}
}
public struct AgentSessionDisplayMessage: Codable, Equatable, Sendable {
public var role: String
public var content: String
}
public struct AgentSessionActivity: Codable, Equatable, Sendable {
public var kind: String
public var content: String?
}
public struct OverlayVisibility: Codable, Equatable, Sendable {
public var petVisible: Bool
public var statusBubbleVisible: Bool
public init(petVisible: Bool = true, statusBubbleVisible: Bool = true) {
self.petVisible = petVisible
self.statusBubbleVisible = statusBubbleVisible
}
enum CodingKeys: String, CodingKey {
case petVisible = "pet_visible"
case statusBubbleVisible = "status_bubble_visible"
}
}
public struct GenerationMessage: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var role: String
public var content: String
public var progress: Double
public var createdAt: String
public var kind: String?
public init(id: String = UUID().uuidString, role: String, content: String, progress: Double, createdAt: String, kind: String? = nil) {
self.id = id
self.role = role
self.content = content
self.progress = progress
self.createdAt = createdAt
self.kind = kind
}
enum CodingKeys: String, CodingKey {
case id
case role
case content
case progress
case createdAt = "created_at"
case kind
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
role = try container.decode(String.self, forKey: .role)
content = try container.decode(String.self, forKey: .content)
progress = try container.decode(Double.self, forKey: .progress)
createdAt = try container.decode(String.self, forKey: .createdAt)
kind = try container.decodeIfPresent(String.self, forKey: .kind)
let suppliedID = try container.decodeIfPresent(String.self, forKey: .id)?
.trimmingCharacters(in: .whitespacesAndNewlines)
id = suppliedID.flatMap { $0.isEmpty ? nil : $0 }
?? Self.legacyID(
role: role,
content: content,
progress: progress,
createdAt: createdAt,
kind: kind
)
}
private static func legacyID(
role: String,
content: String,
progress: Double,
createdAt: String,
kind: String?
) -> String {
let canonical = [
role,
content,
String(progress.bitPattern, radix: 16),
createdAt,
kind ?? "",
].joined(separator: "\u{1F}")
var hash: UInt64 = 14_695_981_039_346_656_037
for byte in canonical.utf8 {
hash ^= UInt64(byte)
hash &*= 1_099_511_628_211
}
return "msg_legacy_\(String(hash, radix: 16))"
}
}
public enum GenerationConversation {
private static let completedKind = "generation_completed"
private static let failedKind = "generation_failed"
private static let canceledKind = "generation_canceled"
private static let inputRequestKind = "input_request"
private static let terminalKinds: Set<String> = [completedKind, failedKind, canceledKind]
public static func succeeded(_ messages: [GenerationMessage]) -> Bool {
latestTerminalKind(messages) == completedKind
}
public static func needsUserInput(_ messages: [GenerationMessage]) -> Bool {
guard let lastMessage = messages.last else { return false }
return lastMessage.role == "assistant" && lastMessage.kind == inputRequestKind
}
public static func terminalUnsuccessful(_ messages: [GenerationMessage]) -> Bool {
guard let kind = latestTerminalKind(messages) else { return false }
return kind == failedKind || kind == canceledKind
}
public static func cancelled(_ messages: [GenerationMessage]) -> Bool {
latestTerminalKind(messages) == canceledKind
}
public static func failed(_ messages: [GenerationMessage]) -> Bool {
latestTerminalKind(messages) == failedKind
}
public static func canSendReply(_ messages: [GenerationMessage]) -> Bool {
succeeded(messages) || needsUserInput(messages)
}
public static func activeStepIndex(messages: [GenerationMessage], progress: Double) -> Int {
if succeeded(messages) {
return 3
}
if needsUserInput(messages) {
return 1
}
if terminalUnsuccessful(messages) {
return 2
}
switch progress {
case 0..<0.25:
return 0
case 0.25..<0.60:
return 1
case 0.60..<0.96:
return 2
default:
return 3
}
}
private static func latestTerminalKind(_ messages: [GenerationMessage]) -> String? {
guard let message = messages.last, message.role == "assistant" else {
return nil
}
if let kind = message.kind, terminalKinds.contains(kind) {
return kind
}
if message.progress >= 1 {
if legacyCompletion(message.content) {
return completedKind
}
if legacyCancelation(message.content) {
return canceledKind
}
if message.kind != inputRequestKind {
return failedKind
}
}
return nil
}
private static func legacyCompletion(_ content: String) -> Bool {
content.contains("完成,可在宠物库启用")
|| content.contains("已保存入库并已启用")
|| content.contains("petpack-source,并已启用")
}
private static func legacyCancelation(_ content: String) -> Bool {
content.contains("已取消生成")
}
}
public enum GenerationOperation: String, Codable, Hashable, Sendable {
case create
case modify
}
public struct GenerationHistory: Codable, Sendable {
public var found: Bool
public var petId: String
public var jobId: String?
public var status: String?
public var sessionId: String?
public var resultPetId: String?
public var retryOfJobId: String?
public var operation: GenerationOperation?
public var createdAt: String?
public var updatedAt: String?
public var form: GenerationForm?
public var messages: [GenerationMessage]
public init(
found: Bool,
petId: String,
jobId: String? = nil,
status: String? = nil,
sessionId: String? = nil,
resultPetId: String? = nil,
retryOfJobId: String? = nil,
operation: GenerationOperation? = nil,
createdAt: String? = nil,
updatedAt: String? = nil,
form: GenerationForm? = nil,
messages: [GenerationMessage] = []
) {
self.found = found
self.petId = petId
self.jobId = jobId
self.status = status
self.sessionId = sessionId
self.resultPetId = resultPetId
self.retryOfJobId = retryOfJobId
self.operation = operation
self.createdAt = createdAt
self.updatedAt = updatedAt
self.form = form
self.messages = messages
}
enum CodingKeys: String, CodingKey {
case found
case petId = "pet_id"
case jobId = "job_id"
case status
case sessionId = "session_id"
case resultPetId = "result_pet_id"
case retryOfJobId = "retry_of_job_id"
case operation
case createdAt = "created_at"
case updatedAt = "updated_at"
case form
case messages
}
}
public enum ActiveGenerationStatus: String, Codable, Hashable, Sendable {
case pending
case running
case waitingForUser = "waiting_for_user"
}
public struct ActiveGenerationSnapshot: Codable, Equatable, Sendable {
public var jobID: String
public var status: ActiveGenerationStatus
public var form: GenerationForm
public var sessionID: String?
public var resultPetID: String?
public var operation: GenerationOperation?
public var ownerInstanceID: String?
public var heartbeatAt: String
public var messageRevision: String
public var messages: [GenerationMessage]
public var inputRequest: GenerationMessage?
public init(
jobID: String,
status: ActiveGenerationStatus,
form: GenerationForm,
sessionID: String? = nil,
resultPetID: String? = nil,
operation: GenerationOperation? = nil,
ownerInstanceID: String? = nil,
heartbeatAt: String,
messageRevision: String,
messages: [GenerationMessage],
inputRequest: GenerationMessage? = nil
) {
self.jobID = jobID
self.status = status
self.form = form
self.sessionID = sessionID
self.resultPetID = resultPetID
self.operation = operation
self.ownerInstanceID = ownerInstanceID
self.heartbeatAt = heartbeatAt
self.messageRevision = messageRevision
self.messages = messages
self.inputRequest = inputRequest
}
enum CodingKeys: String, CodingKey {
case jobID = "job_id"
case status
case form
case sessionID = "session_id"
case resultPetID = "result_pet_id"
case operation
case ownerInstanceID = "owner_instance_id"
case heartbeatAt = "heartbeat_at"
case messageRevision = "message_revision"
case messages
case inputRequest = "input_request"
}
}
public struct ConnectionCheckItem: Codable, Identifiable, Hashable, Sendable {
public var id: String { name }
public var name: String
public var status: CheckStatus
public var detail: String
public init(name: String, status: CheckStatus, detail: String) {
self.name = name
self.status = status
self.detail = detail
}
}
public enum CheckStatus: String, Codable, Hashable, Sendable {
case ok
case needsFix = "needs_fix"
case missing
case unverified
case unsupported
case notRequired = "not_required"
public var title: String {
switch self {
case .ok: "正常"
case .needsFix: "需修复"
case .missing: "未检测到"
case .unverified: "未验证"
case .unsupported: "暂不支持"
case .notRequired: "非必需"
}
}
public var isBlocking: Bool {
self == .needsFix || self == .missing
}
}
public enum ConnectionCheckMode: String, Codable, Hashable, Sendable {
case light
case runtime
public var title: String {
switch self {
case .light: "轻量定位"
case .runtime: "完整检查"
}
}
}
public struct AgentConnectionStatus: Codable, Identifiable, Hashable, Sendable {
public var id: AgentSource { source }
public var source: AgentSource
public var items: [ConnectionCheckItem]
public var installPaths: [String]
public var connectorInstalled: Bool?
public var checkMode: ConnectionCheckMode
public var checkedAt: String?
public init(
source: AgentSource,
items: [ConnectionCheckItem],
installPaths: [String],
connectorInstalled: Bool? = nil,
checkMode: ConnectionCheckMode = .runtime,
checkedAt: String? = nil
) {
self.source = source
self.items = items
self.installPaths = installPaths
self.connectorInstalled = connectorInstalled
self.checkMode = checkMode
self.checkedAt = checkedAt
}
enum CodingKeys: String, CodingKey {
case source
case items
case installPaths = "install_paths"
case connectorInstalled = "connector_installed"
case checkMode = "check_mode"
case checkedAt = "checked_at"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
source = try container.decode(AgentSource.self, forKey: .source)
items = try container.decode([ConnectionCheckItem].self, forKey: .items)
installPaths = try container.decode([String].self, forKey: .installPaths)
connectorInstalled = try container.decodeIfPresent(Bool.self, forKey: .connectorInstalled)
checkMode = try container.decodeIfPresent(ConnectionCheckMode.self, forKey: .checkMode) ?? .runtime
checkedAt = try container.decodeIfPresent(String.self, forKey: .checkedAt)
}
public var hasInstalledConnectorArtifacts: Bool {
if let connectorInstalled {
return connectorInstalled
}
return items.contains { item in
connectorArtifactItemNames.contains(item.name)
&& (item.status == .ok || item.detail.contains("已安装"))
}
}
public var hasRepairableConnectorIssue: Bool {
!installPaths.isEmpty && items.contains { item in
repairableConnectorItemNames.contains(item.name) && item.status.isBlocking
}
}
public var blockingItems: [ConnectionCheckItem] {
items.filter { $0.status.isBlocking }
}
public var unverifiedItems: [ConnectionCheckItem] {
items.filter { $0.status == .unverified }
}
public var unsupportedItems: [ConnectionCheckItem] {
items.filter { $0.status == .unsupported }
}
private var connectorArtifactItemNames: Set<String> {
switch source {
case .codex:
return ["插件源", "Hook", "Pet Studio Skill", "Codex marketplace", "Codex 插件安装"]
case .claudeCode:
return ["Hooks", "事件通道", "Claude settings.json"]
case .pi:
return ["Extension"]
case .opencode:
return ["Plugin"]
}
}
private var repairableConnectorItemNames: Set<String> {
connectorArtifactItemNames
}
}
public struct GenerationForm: Codable, Equatable, Sendable {
public var description: String
public var style: String
public var quality: QualityLevel
public var referenceImages: [String]
public init(description: String, style: String, quality: QualityLevel, referenceImages: [String]) {
self.description = description
self.style = style
self.quality = quality
self.referenceImages = referenceImages
}
enum CodingKeys: String, CodingKey {
case description
case style
case quality
case referenceImages = "reference_images"
}
}