import Foundation enum SentenceBoundaryAPIError: LocalizedError { case invalidHash case hashMismatch var errorDescription: String? { switch self { case .invalidHash: return "video_hash 必须是 64 位 SHA-256 十六进制字符串" case .hashMismatch: return "服务端返回的句边界哈希与请求不一致" } } } enum SentenceBoundaryAPI { static func fetch(videoHash: String) async throws -> SentenceBoundaryDocument { let hash = videoHash.lowercased() guard hash.range(of: AppConfig.sha256Pattern, options: .regularExpression) != nil else { throw SentenceBoundaryAPIError.invalidHash } var headers: [String: String] = [:] if !AppConfig.authToken.isEmpty { headers["Authorization"] = "Bearer \(AppConfig.authToken)" } let result = try await HTTPClient.getJSON( SentenceBoundaryDocument.self, baseURL: AppConfig.serverBaseURL, path: "api/v1/videos/\(hash)/sentence-boundaries", timeout: AppConfig.readTimeout, additionalHeaders: headers ) guard result.value.videoHash.lowercased() == hash else { throw SentenceBoundaryAPIError.hashMismatch } return result.value } } struct SentenceExplanationResponse: Decodable { let explanation: String private enum CodingKeys: String, CodingKey { case explanation } } extension SentenceBoundaryAPI { static func explain( _ sentence: SentenceBoundary, courseTitle: String ) async throws -> String { var body: [String: Any] = [ "sentence_text": sentence.text ?? "", "course_title": courseTitle, "sentence_index": sentence.index, ] if let language = sentence.language { body["language"] = language } var headers: [String: String] = ["Content-Type": "application/json"] if !AppConfig.authToken.isEmpty { headers["Authorization"] = "Bearer \(AppConfig.authToken)" } let result = try await HTTPClient.execute( baseURL: AppConfig.serverBaseURL, path: "api/v1/sentences/explain", method: "POST", timeout: AppConfig.readTimeout, headers: headers, body: try JSONSerialization.data(withJSONObject: body) ) guard (200...299).contains(result.statusCode) else { let detail = String(data: result.body, encoding: .utf8) ?? "" throw HTTPClientError.badStatus(result.statusCode, detail) } return try JSONDecoder() .decode(SentenceExplanationResponse.self, from: result.body) .explanation } }