feat(keyboard): expand contextual skills and managed flows

Add local clipboard intent recommendations, webpage and phone actions, and safer host handoffs. Refine managed gateway, catalog refresh, onboarding, and adaptive polish behavior.
This commit is contained in:
Rocky
2026-08-23 14:02:33 +08:00
parent 7df5bbdaa0
commit 2cc81c4628
68 changed files with 10368 additions and 7765 deletions
@@ -43,8 +43,12 @@ public struct AIAgentSkillLayout: Codable, Equatable, Sendable {
) -> AIAgentSkillLayout {
let known = Dictionary(uniqueKeysWithValues: catalog.map { ($0.id, $0) })
var seenEnabled = Set<String>()
let enabled = enabledIDs.filter { id in
known[id] != nil && seenEnabled.insert(id).inserted
let enabled = enabledIDs.compactMap { id -> String? in
let canonical = AIClipboardSkillCatalog.canonicalID(for: id)
guard known[canonical] != nil, seenEnabled.insert(canonical).inserted else {
return nil
}
return canonical
}
var seenConfirmed = Set<String>()
+12 -2
View File
@@ -46,6 +46,9 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
public var locale: String
public var conditions: [String]
public var metadata: AIHintMetadata?
/// Server policy intent. Ordinary cards match hold-to-talk AI; current
/// information cards additionally require online search.
public var taskKind: ManagedGatewayTaskKind
public init(
id: String,
@@ -56,7 +59,8 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
source: String = "local",
locale: String = "zh",
conditions: [String] = [],
metadata: AIHintMetadata? = nil
metadata: AIHintMetadata? = nil,
taskKind: ManagedGatewayTaskKind = .aiQuestion
) {
self.id = id
self.displayText = displayText
@@ -67,6 +71,7 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
self.locale = locale
self.conditions = conditions
self.metadata = metadata
self.taskKind = taskKind
}
public var requiresClipboard30s: Bool {
@@ -83,7 +88,7 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
}
enum CodingKeys: String, CodingKey {
case id, displayText, text, prompt, category, priority, source, locale, conditions, metadata
case id, displayText, text, prompt, category, priority, source, locale, conditions, metadata, taskKind
}
public init(from decoder: Decoder) throws {
@@ -96,6 +101,10 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
locale = try container.decodeIfPresent(String.self, forKey: .locale) ?? "zh"
conditions = try container.decodeIfPresent([String].self, forKey: .conditions) ?? []
metadata = try container.decodeIfPresent(AIHintMetadata.self, forKey: .metadata)
taskKind = try container.decodeIfPresent(
ManagedGatewayTaskKind.self,
forKey: .taskKind
) ?? .aiQuestion
if let display = try container.decodeIfPresent(String.self, forKey: .displayText),
!display.isEmpty {
displayText = display
@@ -115,6 +124,7 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
try container.encode(locale, forKey: .locale)
try container.encode(conditions, forKey: .conditions)
try container.encodeIfPresent(metadata, forKey: .metadata)
try container.encode(taskKind, forKey: .taskKind)
}
}
@@ -77,7 +77,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
public static let localASRCustomLanguageModelEnabled = "config.localASR.customLanguageModelEnabled"
/// Enabled AI Agent skill IDs (order) + confirmed companion Shortcuts.
public static let agentSkillLayout = "config.aiAgentSkills.layout.v1"
/// One-shot: append semantic built-ins introduced with the unlimited layout.
/// One-shot migrations for newly default-installed built-in and official skills.
public static let agentSkillDefaultsMigrationVersion =
"config.aiAgentSkills.defaultsMigrationVersion"
/// User-created clipboard skills (no cloud sync; App Group only).
@@ -20,8 +20,8 @@ public struct FlowCommand: Codable, Equatable, Sendable {
case submitAIQuestion
}
/// Wire version that includes a strongly typed OOBE feature.
public static let currentProtocolVersion = 8
/// Wire version that includes host-side webpage summary extraction.
public static let currentProtocolVersion = 10
public let protocolVersion: Int
public let sessionId: UUID
@@ -41,8 +41,12 @@ public struct FlowCommand: Codable, Equatable, Sendable {
public let aiConversationID: UUID?
/// Prefilled question used only by `.submitAIQuestion`.
public let aiQuestionText: String?
/// Public webpage fetched by the host for an explicit summary skill.
public let aiWebPageURL: URL?
/// Fine-grained managed-gateway intent for AI question submissions.
public let aiTaskKind: ManagedGatewayTaskKind?
/// Audited product entry point for managed usage attribution.
public let managedRequestSource: ManagedGatewayRequestSource?
/// Optional server-audited purpose for managed gateway billing policy.
public let managedRequestPurpose: ManagedGatewayRequestPurpose?
/// Required feature discriminator when `managedRequestPurpose == .oobe`.
@@ -68,7 +72,9 @@ public struct FlowCommand: Codable, Equatable, Sendable {
sourceHistoryEntryRevision: Int64? = nil,
aiConversationID: UUID? = nil,
aiQuestionText: String? = nil,
aiWebPageURL: URL? = nil,
aiTaskKind: ManagedGatewayTaskKind? = nil,
managedRequestSource: ManagedGatewayRequestSource? = nil,
managedRequestPurpose: ManagedGatewayRequestPurpose? = nil,
managedOOBEFeature: ManagedGatewayOOBEFeature? = nil,
aiThinkingEnabled: Bool? = nil,
@@ -89,7 +95,9 @@ public struct FlowCommand: Codable, Equatable, Sendable {
self.sourceHistoryEntryRevision = sourceHistoryEntryRevision
self.aiConversationID = aiConversationID
self.aiQuestionText = aiQuestionText
self.aiWebPageURL = aiWebPageURL
self.aiTaskKind = aiTaskKind
self.managedRequestSource = managedRequestSource
self.managedRequestPurpose = managedRequestPurpose
self.managedOOBEFeature = managedOOBEFeature
self.aiThinkingEnabled = aiThinkingEnabled
@@ -13,8 +13,12 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
public let aiConversationID: UUID?
/// When set with `.aiQuestion`, host skips ASR and answers this text.
public let aiQuestionText: String?
/// Public HTTPS page to fetch in the host before a webpage-summary request.
public let aiWebPageURL: URL?
/// Fine-grained managed-gateway intent. Regular questions keep the default.
public let aiTaskKind: ManagedGatewayTaskKind?
/// Audited product entry point for managed usage attribution.
public let managedRequestSource: ManagedGatewayRequestSource?
/// Optional server-audited purpose for managed gateway billing policy.
public let managedRequestPurpose: ManagedGatewayRequestPurpose?
/// Required feature discriminator when `managedRequestPurpose == .oobe`.
@@ -31,7 +35,9 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
sourceHistoryEntryRevision: Int64? = nil,
aiConversationID: UUID? = nil,
aiQuestionText: String? = nil,
aiWebPageURL: URL? = nil,
aiTaskKind: ManagedGatewayTaskKind? = nil,
managedRequestSource: ManagedGatewayRequestSource? = nil,
managedRequestPurpose: ManagedGatewayRequestPurpose? = nil,
managedOOBEFeature: ManagedGatewayOOBEFeature? = nil,
aiThinkingEnabled: Bool? = nil
@@ -42,7 +48,9 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
self.sourceHistoryEntryRevision = sourceHistoryEntryRevision
self.aiConversationID = aiConversationID
self.aiQuestionText = aiQuestionText
self.aiWebPageURL = aiWebPageURL
self.aiTaskKind = aiTaskKind
self.managedRequestSource = managedRequestSource
self.managedRequestPurpose = managedRequestPurpose
self.managedOOBEFeature = managedOOBEFeature
self.aiThinkingEnabled = aiThinkingEnabled
@@ -66,14 +74,18 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
conversationID: UUID,
prefilledQuestion: String? = nil,
taskKind: ManagedGatewayTaskKind = .aiQuestion,
requestSource: ManagedGatewayRequestSource? = nil,
oobeFeature: ManagedGatewayOOBEFeature? = nil,
thinkingEnabled: Bool? = nil
thinkingEnabled: Bool? = nil,
webPageURL: URL? = nil
) -> FlowUtteranceRequest {
FlowUtteranceRequest(
mode: .aiQuestion,
aiConversationID: conversationID,
aiQuestionText: prefilledQuestion,
aiWebPageURL: webPageURL,
aiTaskKind: taskKind,
managedRequestSource: requestSource,
managedRequestPurpose: oobeFeature == nil ? nil : .oobe,
managedOOBEFeature: oobeFeature,
aiThinkingEnabled: thinkingEnabled
@@ -85,7 +85,7 @@ public struct OfficialSkillDefinition: Codable, Equatable, Identifiable, Sendabl
cardTitleKey: "",
descriptionKey: "",
kind: kind,
isDefault: false,
isDefault: true,
customName: localization.name,
customSummary: localization.summary,
customPrompt: localization.prompt,
@@ -114,3 +114,76 @@ public enum TranslationLanguageCatalog {
return all.first { $0.id == offLocaleId } ?? all[0]
}
}
/// Resolves the device's first preferred language for clipboard translation.
/// This intentionally ignores the optional post-dictation translation target.
public enum SystemLanguageResolver {
public static func primaryIdentifier(
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
normalizedIdentifier(
preferredLanguages.first ?? Locale.autoupdatingCurrent.identifier
)
}
public static func promptLanguageName(
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
let identifier = primaryIdentifier(preferredLanguages: preferredLanguages)
if let known = TranslationLanguageCatalog.all.first(where: { $0.id == identifier }) {
return known.promptLanguageName
}
return Locale(identifier: "en").localizedString(forIdentifier: identifier)
?? identifier
}
public static func displayLanguageName(
uiLanguage: AppUILanguage,
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
let identifier = primaryIdentifier(preferredLanguages: preferredLanguages)
let displayLocale = Locale(identifier: uiLanguage.resolvedLanguageCode())
return displayLocale.localizedString(forIdentifier: identifier)
?? promptLanguageName(preferredLanguages: preferredLanguages)
}
public static func isSameLanguage(
sourceIdentifier: String,
targetIdentifier: String
) -> Bool {
let source = normalizedIdentifier(sourceIdentifier)
let target = normalizedIdentifier(targetIdentifier)
if source.hasPrefix("zh"), target.hasPrefix("zh") {
let sourceScript = chineseScript(in: source)
let targetScript = chineseScript(in: target)
return sourceScript == nil || targetScript == nil || sourceScript == targetScript
}
return source == target
}
private static func normalizedIdentifier(_ identifier: String) -> String {
let language = Locale.Language(identifier: identifier)
guard let rawCode = language.languageCode?.identifier else {
return identifier.lowercased()
}
let code = rawCode.lowercased()
guard code == "zh" || code == "yue" else { return code }
let script = language.script?.identifier.lowercased()
let region = Locale(identifier: identifier).region?.identifier.uppercased()
if script == "hant" || ["HK", "MO", "TW"].contains(region) {
return "zh-Hant"
}
if script == "hans" || ["CN", "MY", "SG"].contains(region) {
return "zh-Hans"
}
return "zh"
}
private static func chineseScript(in identifier: String) -> String? {
let normalized = identifier.lowercased()
if normalized.contains("hant") { return "Hant" }
if normalized.contains("hans") { return "Hans" }
return nil
}
}