完善首页权限申请流程与队列播报体验。

对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 13:46:45 +08:00
parent c083f1d4b3
commit efb3925257
50 changed files with 2585 additions and 455 deletions

View File

@ -304,7 +304,15 @@ final class ScenicQueueFeatureTests: XCTestCase {
let tokenProvider = MockAliyunNLSTokenProvider()
let bridge = MockAliyunNuiTTSBridge()
let player = MockAliyunPCMStreamingPlayer()
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.onlineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.onlineTTS")
let store = AppStore(defaults: defaults)
store.session.userId = "u1"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
store.scenicQueue.useOfflineTTS = false
let manager = AliyunNLSTTSManager(
appStore: store,
tokenProvider: tokenProvider,
bridge: bridge,
streamingPlayer: player
@ -321,6 +329,58 @@ final class ScenicQueueFeatureTests: XCTestCase {
XCTAssertEqual(Array(player.events.suffix(3)), ["start:16000", "write:4", "drain"])
}
func testAliyunTTSManagerInitializesOfflineEngineWithBundledVoice() async throws {
let api = MockScenicQueueAPI()
let tokenProvider = MockAliyunNLSTokenProvider()
let bridge = MockAliyunNuiTTSBridge()
bridge.paramValues["model_sample_rate"] = "24000"
let player = MockAliyunPCMStreamingPlayer()
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.nativeOfflineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.nativeOfflineTTS")
let store = AppStore(defaults: defaults)
store.session.userId = "u1"
store.session.accountType = .unknown("photog")
store.session.currentScenicId = 11
store.scenicQueue.useOfflineTTS = true
let workspace = FileManager.default.temporaryDirectory
.appendingPathComponent("ScenicQueueFeatureTests-\(UUID().uuidString)", isDirectory: true)
let voiceDirectory = workspace.appendingPathComponent("tts/voices", isDirectory: true)
try FileManager.default.createDirectory(at: voiceDirectory, withIntermediateDirectories: true)
try Data([0]).write(to: workspace.appendingPathComponent("tts/languagedata_embedded.bin"))
try Data([0]).write(to: workspace.appendingPathComponent("tts/parameter.cfg"))
try Data([0]).write(to: voiceDirectory.appendingPathComponent("aijia"))
defer { try? FileManager.default.removeItem(at: workspace) }
let manager = AliyunNLSTTSManager(
appStore: store,
tokenProvider: tokenProvider,
bridge: bridge,
streamingPlayer: player,
workspacePathProvider: { workspace.path }
)
XCTAssertTrue(manager.supportsOfflineTTS)
await manager.prepareQueueTTSEngine(api: api)
let ok = await manager.speakPlainTextOnce("请到拍摄区域")
XCTAssertTrue(ok)
let ticketData = try XCTUnwrap(bridge.initializedTickets.last?.data(using: .utf8))
let ticket = try XCTUnwrap(JSONSerialization.jsonObject(with: ticketData) as? [String: String])
XCTAssertEqual(ticket["app_key"], "gfWimmOuTku1AbIV")
XCTAssertEqual(ticket["sdk_code"], "software_nls_tts_offline_standard")
XCTAssertEqual(ticket["mode_type"], "0")
XCTAssertEqual(ticket["workspace"], workspace.path)
XCTAssertEqual(ticket["ak_id"], "ak")
XCTAssertEqual(ticket["sts_token"], "security-token")
XCTAssertTrue(bridge.events.contains { $0.hasPrefix("set:extend_font_name=") && $0.hasSuffix("/tts/voices/aijia") })
XCTAssertTrue(bridge.events.contains("set:encode_type=pcm"))
XCTAssertTrue(bridge.events.contains("set:sample_rate=24000"))
XCTAssertTrue(bridge.events.contains("set:speed_level=1.0"))
XCTAssertFalse(bridge.events.contains("set:mode_type=2"))
XCTAssertEqual(Array(player.events.suffix(3)), ["start:24000", "write:4", "drain"])
}
func testSettingsOfflineTTSSwitchReportsUnavailable() {
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.offlineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.offlineTTS")
@ -471,10 +531,13 @@ private final class MockAliyunNLSTokenProvider: AliyunNLSTokenProviding {
private final class MockAliyunNuiTTSBridge: AliyunNuiTTSBridging {
weak var delegate: AliyunNuiTTSBridgeDelegate?
var sdkAvailable = true
var paramValues: [String: String] = [:]
private(set) var events: [String] = []
private(set) var initializedTickets: [String] = []
private let callbackBridge = AliyunNuiTTSBridge()
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
initializedTickets.append(ticket)
events.append("initialize:\(ticket.contains("mock-nls-token"))")
return 0
}
@ -507,7 +570,7 @@ private final class MockAliyunNuiTTSBridge: AliyunNuiTTSBridging {
}
func paramValue(for key: String) -> String? {
nil
paramValues[key]
}
}