feat(account): add managed credits and cloud gateway

Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
This commit is contained in:
Rocky
2026-08-20 11:43:21 +08:00
parent 0f9280bd00
commit 498f407585
301 changed files with 19221 additions and 10891 deletions
+37 -21
View File
@@ -1,8 +1,8 @@
// FlowSessionBridgeTests.swift
// OSGKeyboardTests
import XCTest
@testable import OSGKeyboardShared
import XCTest
final class FlowSessionBridgeTests: XCTestCase {
private var suiteNames: Set<String> = []
@@ -313,7 +313,8 @@ final class FlowSessionBridgeTests: XCTestCase {
localeId: "zh-Hans",
utteranceMode: .aiQuestion,
aiConversationID: UUID(),
aiQuestionText: "总结这段剪贴板内容"
aiQuestionText: "总结这段剪贴板内容",
aiTaskKind: .clipboardTransform
)
let decoded = try JSONDecoder().decode(
@@ -323,6 +324,7 @@ final class FlowSessionBridgeTests: XCTestCase {
XCTAssertEqual(decoded, command)
XCTAssertEqual(decoded.aiQuestionText, "总结这段剪贴板内容")
XCTAssertEqual(decoded.aiTaskKind, .clipboardTransform)
XCTAssertNil(decoded.aiThinkingEnabled)
}
@@ -345,7 +347,19 @@ final class FlowSessionBridgeTests: XCTestCase {
XCTAssertEqual(decoded.aiThinkingEnabled, false)
}
func testFlowCommandDecodesLegacyJSONWithoutThinkingKey() throws {
func testAIQuestionRequestCarriesManagedTaskIntent() {
let request = FlowUtteranceRequest.aiQuestion(
conversationID: UUID(),
prefilledQuestion: "整理剪贴板",
taskKind: .customSkill,
thinkingEnabled: false
)
XCTAssertEqual(request.aiTaskKind, .customSkill)
XCTAssertEqual(request.aiThinkingEnabled, false)
}
func testFlowCommandDecodesLegacyJSONWithoutAIIntentKeys() throws {
let command = FlowCommand(
sessionId: UUID(),
utteranceId: UUID(),
@@ -360,9 +374,11 @@ final class FlowSessionBridgeTests: XCTestCase {
with: JSONEncoder().encode(command)
) as! [String: Any]
object.removeValue(forKey: "aiThinkingEnabled")
object.removeValue(forKey: "aiTaskKind")
let data = try JSONSerialization.data(withJSONObject: object)
let decoded = try JSONDecoder().decode(FlowCommand.self, from: data)
XCTAssertNil(decoded.aiThinkingEnabled)
XCTAssertNil(decoded.aiTaskKind)
XCTAssertEqual(decoded.aiQuestionText, "总结这段剪贴板内容")
}
@@ -960,7 +976,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.flowAckPayload,
FlowSessionKeys.flowStartTransactionPayload,
FlowSessionKeys.pendingKeyboardUtteranceId,
FlowSessionKeys.flowReadyPayload,
FlowSessionKeys.flowReadyPayload
],
in: defaults
)
@@ -975,7 +991,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.lastActivityAt,
FlowSessionKeys.hostGeneration,
FlowSessionKeys.hostHeavy,
FlowSessionKeys.hostHeavyAt,
FlowSessionKeys.hostHeavyAt
],
in: defaults
)
@@ -1012,7 +1028,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.flowAckPayload,
FlowSessionKeys.flowStartTransactionPayload,
FlowSessionKeys.pendingKeyboardUtteranceId,
FlowSessionKeys.flowReadyPayload,
FlowSessionKeys.flowReadyPayload
],
in: defaults
)
@@ -1021,7 +1037,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.flowSessionActive,
FlowSessionKeys.lastPiPArmAttemptAt,
FlowSessionKeys.hostGeneration,
FlowSessionKeys.hostHeavy,
FlowSessionKeys.hostHeavy
],
in: defaults
)
@@ -1056,7 +1072,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.flowAckPayload,
FlowSessionKeys.flowStartTransactionPayload,
FlowSessionKeys.pendingKeyboardUtteranceId,
FlowSessionKeys.flowReadyPayload,
FlowSessionKeys.flowReadyPayload
],
in: defaults
)
@@ -1067,7 +1083,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.pendingHostBundleId,
FlowSessionKeys.lastPiPArmAttemptAt,
FlowSessionKeys.hostGeneration,
FlowSessionKeys.hostHeavy,
FlowSessionKeys.hostHeavy
],
in: defaults
)
@@ -1161,7 +1177,7 @@ final class FlowSessionBridgeTests: XCTestCase {
let defaults = makeDefaults()
let legacyLevels = [
NSNumber(value: Float(0.25)),
NSNumber(value: Float(0.75)),
NSNumber(value: Float(0.75))
]
defaults.set(legacyLevels, forKey: FlowSessionKeys.audioLevels)
@@ -1220,7 +1236,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.transcriptionPartial,
FlowSessionKeys.transcriptionPolishWarning,
FlowSessionKeys.transcriptionError,
FlowSessionKeys.transcriptionErrorKind,
FlowSessionKeys.transcriptionErrorKind
],
in: defaults
)
@@ -1239,7 +1255,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowSessionKeys.flowAckPayload,
FlowSessionKeys.flowStartTransactionPayload,
FlowSessionKeys.pendingKeyboardUtteranceId,
FlowSessionKeys.flowReadyPayload,
FlowSessionKeys.flowReadyPayload
],
in: defaults
)
@@ -1326,7 +1342,7 @@ final class FlowSessionBridgeTests: XCTestCase {
startDeadlineAt: 1_700_000_008.25,
processingDeadlineAt: 1_700_000_045.25
)
let expected = #"{"action":"startRecording","aiConversationID":"33333333-4444-5555-6666-777777777777","commandSeq":42,"createdAt":1700000000.25,"editSourceText":"draft","fieldContext":{"followingText":"after","isContextAvailable":true,"isEmptyField":false,"isSecureEntry":false,"keyboardType":"default","precedingText":"before","returnKeyType":"send"},"localeId":"en-US","processingDeadlineAt":1700000045.25,"protocolVersion":5,"sessionId":"00112233-4455-6677-8899-AABBCCDDEEFF","sourceHistoryEntryID":"22222222-3333-4444-5555-666666666666","sourceHistoryEntryRevision":7,"startDeadlineAt":1700000008.25,"utteranceId":"11111111-2222-3333-4444-555555555555","utteranceMode":"editLastInput"}"#
let expected = #"{"action":"startRecording","aiConversationID":"33333333-4444-5555-6666-777777777777","commandSeq":42,"createdAt":1700000000.25,"editSourceText":"draft","fieldContext":{"followingText":"after","isContextAvailable":true,"isEmptyField":false,"isSecureEntry":false,"keyboardType":"default","precedingText":"before","returnKeyType":"send"},"localeId":"en-US","processingDeadlineAt":1700000045.25,"protocolVersion":6,"sessionId":"00112233-4455-6677-8899-AABBCCDDEEFF","sourceHistoryEntryID":"22222222-3333-4444-5555-666666666666","sourceHistoryEntryRevision":7,"startDeadlineAt":1700000008.25,"utteranceId":"11111111-2222-3333-4444-555555555555","utteranceMode":"editLastInput"}"#
XCTAssertEqual(try sortedJSONString(command), expected)
}
@@ -1362,7 +1378,7 @@ final class FlowSessionBridgeTests: XCTestCase {
historyEntryRevision: 9,
aiConversationID: conversationID
)
let expected = #"{"aiConversationID":"33333333-4444-5555-6666-777777777777","commandSeq":42,"createdAt":1700000050.5,"errorKind":"asrFailed","fieldFingerprint":"default|send|before|after","historyEntryID":"22222222-3333-4444-5555-666666666666","historyEntryRevision":9,"hostGeneration":"generation-1","protocolVersion":5,"rawText":"raw","revision":8,"sessionId":"00112233-4455-6677-8899-AABBCCDDEEFF","status":"final","text":"polished","utteranceId":"11111111-2222-3333-4444-555555555555","utteranceMode":"aiQuestion","warning":"fallback"}"#
let expected = #"{"aiConversationID":"33333333-4444-5555-6666-777777777777","commandSeq":42,"createdAt":1700000050.5,"errorKind":"asrFailed","fieldFingerprint":"default|send|before|after","historyEntryID":"22222222-3333-4444-5555-666666666666","historyEntryRevision":9,"hostGeneration":"generation-1","protocolVersion":6,"rawText":"raw","revision":8,"sessionId":"00112233-4455-6677-8899-AABBCCDDEEFF","status":"final","text":"polished","utteranceId":"11111111-2222-3333-4444-555555555555","utteranceMode":"aiQuestion","warning":"fallback"}"#
XCTAssertEqual(try sortedJSONString(result), expected)
}
@@ -1442,7 +1458,7 @@ final class FlowSessionBridgeTests: XCTestCase {
.primeAudio,
.cancelPrimeAudio,
.endAIConversation,
.submitAIQuestion,
.submitAIQuestion
].map(\.rawValue),
[
"startRecording",
@@ -1452,7 +1468,7 @@ final class FlowSessionBridgeTests: XCTestCase {
"primeAudio",
"cancelPrimeAudio",
"endAIConversation",
"submitAIQuestion",
"submitAIQuestion"
]
)
XCTAssertEqual(
@@ -1463,7 +1479,7 @@ final class FlowSessionBridgeTests: XCTestCase {
.final,
.error,
.aborted,
.timeout,
.timeout
].map(\.rawValue),
["partial", "rawReady", "streaming", "final", "error", "aborted", "timeout"]
)
@@ -1471,7 +1487,7 @@ final class FlowSessionBridgeTests: XCTestCase {
[
FlowAck.DeliveryOutcome.replaced,
.appended,
.rejected,
.rejected
].map(\.rawValue),
["replaced", "appended", "rejected"]
)
@@ -1480,7 +1496,7 @@ final class FlowSessionBridgeTests: XCTestCase {
FlowStartTransaction.Phase.issued,
.starting,
.recording,
.terminal,
.terminal
].map(\.rawValue),
["issued", "starting", "recording", "terminal"]
)
@@ -1497,7 +1513,7 @@ final class FlowSessionBridgeTests: XCTestCase {
.permissionMissing,
.appGroupUnavailable,
.hostLost,
.error,
.error
].map(\.rawValue),
[
"ready",
@@ -1511,7 +1527,7 @@ final class FlowSessionBridgeTests: XCTestCase {
"permissionMissing",
"appGroupUnavailable",
"hostLost",
"error",
"error"
]
)
}