import Foundation // 控制中心控件用的原生剪贴板客户端:用控件专用设备会话(WidgetSessionStore)直连 // /api/clipboard 读写,access 过期(401)即调 /api/auth/refresh 自刷并写回。控件在独立进程 // 运行、不开主 app,故不能复用引擎桥——这里走纯原生 REST(PLAN §I4)。独立会话使自刷不与 // 引擎抢 refresh 轮换。 enum ClipboardClient { enum ClientError: Error { case noSession, http(Int) } // 上传本机剪贴板文本到云剪贴板(PUT /api/clipboard)。 static func upload(_ text: String) async throws { let body = try JSONSerialization.data(withJSONObject: [ "content": text, "content_type": "text/plain", "origin_ts": Int(Date().timeIntervalSince1970 * 1000), ]) _ = try await authed(method: "PUT", path: "/api/clipboard", body: body) } // 拉取云剪贴板文本(GET /api/clipboard)。 static func pull() async throws -> String { let data = try await authed(method: "GET", path: "/api/clipboard", body: nil) let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] return (obj?["content"] as? String) ?? "" } // 带鉴权 + 401 自刷重试一次的请求。 private static func authed(method: String, path: String, body: Data?) async throws -> Data { guard var session = WidgetSessionStore.load() else { throw ClientError.noSession } do { return try await send(method: method, path: path, body: body, token: session.accessToken) } catch ClientError.http(401) { session = try await refresh(session) WidgetSessionStore.save(session) return try await send(method: method, path: path, body: body, token: session.accessToken) } } private static func send(method: String, path: String, body: Data?, token: String) async throws -> Data { var req = URLRequest(url: URL(string: CDropAPI.base + path)!) req.httpMethod = method req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") if let body { req.httpBody = body req.setValue("application/json", forHTTPHeaderField: "Content-Type") } let (data, resp) = try await URLSession.shared.data(for: req) let code = (resp as? HTTPURLResponse)?.statusCode ?? 0 // 200 与 409(PUT 时云端有更新的 LWW 冲突)都算成功;其余抛对应码(401 触发自刷)。 guard code == 200 || code == 409 else { throw ClientError.http(code) } return data } private static func refresh(_ session: WidgetSession) async throws -> WidgetSession { let body = try JSONSerialization.data(withJSONObject: [ "refresh_token": session.refreshToken ]) var req = URLRequest(url: URL(string: CDropAPI.base + "/api/auth/refresh")!) req.httpMethod = "POST" req.httpBody = body req.setValue("application/json", forHTTPHeaderField: "Content-Type") let (data, resp) = try await URLSession.shared.data(for: req) let code = (resp as? HTTPURLResponse)?.statusCode ?? 0 guard code == 200, let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let access = obj["access_token"] as? String else { // 刷新被拒(refresh 过期 / 吊销)→ 清掉控件会话,待主 app 下次启动重铸。 // CAS-before-clear:仅当磁盘上的 refresh 仍是本次所用那条时才清——否则说明另一个并发 // 控件 intent 已轮换出新会话,别把它刚写入的新令牌误删(审查 L3)。 if WidgetSessionStore.load()?.refreshToken == session.refreshToken { WidgetSessionStore.clear() } throw ClientError.http(401) } let newRefresh = (obj["refresh_token"] as? String) ?? session.refreshToken return WidgetSession(accessToken: access, refreshToken: newRefresh, userId: session.userId) } }