import Foundation final class FeishuService { private let baseURL = "https://open.feishu.cn/open-apis" private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .shared) { self.session = session let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase self.decoder = decoder } func fetchBugs( config: AppConfig, assigneeName: String, accessToken: String ) async throws -> [Bug] { var allRecords: [RecordItem] = [] var pageToken: String? repeat { let page = try await fetchPage( appToken: config.appToken, tableId: config.tableId, pageToken: pageToken, accessToken: accessToken ) allRecords.append(contentsOf: page.items) pageToken = page.hasMore ? page.pageToken : nil } while pageToken != nil return allRecords .map { BugMapper.map($0, config: config) } .filter { bug in bug.assignee.localizedCaseInsensitiveContains(assigneeName) } } func fetchRecordCount( appToken: String, tableId: String, accessToken: String ) async throws -> Int { let page = try await fetchPage( appToken: appToken, tableId: tableId, pageToken: nil, pageSize: 1, accessToken: accessToken ) return page.total ?? page.items.count } private func fetchPage( appToken: String, tableId: String, pageToken: String?, pageSize: Int = 500, accessToken: String ) async throws -> RecordListData { var components = URLComponents( string: "\(baseURL)/bitable/v1/apps/\(appToken)/tables/\(tableId)/records" )! var queryItems = [URLQueryItem(name: "page_size", value: String(pageSize))] if let pageToken { queryItems.append(URLQueryItem(name: "page_token", value: pageToken)) } components.queryItems = queryItems var request = URLRequest(url: components.url!) request.httpMethod = "GET" request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") request.timeoutInterval = 30 let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw FeishuError.networkError(nil) } if httpResponse.statusCode == 401 { throw FeishuError.unauthorized } let apiResponse: FeishuAPIResponse do { apiResponse = try decoder.decode(FeishuAPIResponse.self, from: data) } catch { throw FeishuError.decodingError(error) } guard apiResponse.code == 0, let pageData = apiResponse.data else { throw FeishuError.apiError(code: apiResponse.code, message: apiResponse.msg) } return pageData } }