diff --git a/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt b/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt index 7d3c344..5da36eb 100644 --- a/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt @@ -53,6 +53,26 @@ class WorkBuddyRepository constructor( private val jsonMedia = "application/json".toMediaType() + private fun Credential.WorkBuddyCredential.isIntlRegion(): Boolean = + region.equals("INTL", ignoreCase = true) + + private fun Credential.WorkBuddyCredential.apiBase(): String = + if (isIntlRegion()) "https://www.workbuddy.ai" else "https://copilot.tencent.com" + + private fun Credential.WorkBuddyCredential.originBase(): String = + if (isIntlRegion()) "https://www.workbuddy.ai" else "https://www.codebuddy.cn" + + private fun Credential.WorkBuddyCredential.summaryUrl(): String = + if (isIntlRegion()) "https://www.workbuddy.ai/billing/meter/get-user-resource-summary" + else "https://www.workbuddy.cn/billing/meter/get-user-resource-summary" + + private fun Credential.WorkBuddyCredential.resourceUrl(): String = + if (isIntlRegion()) "https://www.workbuddy.ai/billing/meter/get-user-resource" + else "https://www.codebuddy.cn/v2/billing/meter/get-user-resource" + + private fun Credential.WorkBuddyCredential.refreshUrl(): String = + apiBase() + "/v2/plugin/auth/token/refresh" + private val codebuddyBody = buildJsonObject { put("PageNumber", 1) put("PageSize", 100) @@ -79,7 +99,7 @@ class WorkBuddyRepository constructor( var lastBody = "" while (attempts < 3) { attempts++ - val request = if (hostMode == 0) summaryRequest(access) else codebuddyRequest(access) + val request = if (hostMode == 0) summaryRequest(credential, access) else codebuddyRequest(credential, access) val resp = try { okHttpClient.newCall(request).execute() } catch (e: IOException) { @@ -101,7 +121,7 @@ class WorkBuddyRepository constructor( return@withContext Result.failure(mapHttpError(code, body)) } 401, 403 -> { - val refreshed = tryRefesh(refresh) ?: return@withContext Result.failure( + val refreshed = tryRefesh(credential, refresh) ?: return@withContext Result.failure( RepositoryError.InvalidCredential() ) // 暂存轮换后的凭据,随本次成功提交(RefreshWriteSession 机制) @@ -141,7 +161,7 @@ class WorkBuddyRepository constructor( // 细粒度账户明细(官网个人中心同源数据):每个资源包的已用/总量/剩余/到期时间。 // 独立请求,失败仅降级(保留 summary 的简化 extras),不影响余额主流程。 - val detailExtras = runCatching { fetchResourceDetailExtras(access) }.getOrNull() + val detailExtras = runCatching { fetchResourceDetailExtras(credential, access) }.getOrNull() val mergedExtras = if (detailExtras != null) { parsed.extras + detailExtras } else { @@ -166,9 +186,9 @@ class WorkBuddyRepository constructor( * 拉取细粒度账户明细并转成 extras 增量([KEY_DETAIL_RAW] / [KEY_PLAN_CODE])。 * 请求失败或解析失败返回 null(调用方降级到 summary 的简化 extras)。 */ - private fun fetchResourceDetailExtras(accessToken: String): Map? { + private fun fetchResourceDetailExtras(credential: Credential.WorkBuddyCredential, accessToken: String): Map? { val request = Request.Builder() - .url("https://www.codebuddy.cn/v2/billing/meter/get-user-resource") + .url(credential.resourceUrl()) .addHeader("Authorization", "Bearer $accessToken") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") @@ -187,18 +207,18 @@ class WorkBuddyRepository constructor( } /** workbuddy.cn 官网个人中心实测端点(2026-09 确认 200)。 */ - private fun summaryRequest(accessToken: String): Request = + private fun summaryRequest(credential: Credential.WorkBuddyCredential, accessToken: String): Request = Request.Builder() - .url("https://www.workbuddy.cn/billing/meter/get-user-resource-summary") + .url(credential.summaryUrl()) .addHeader("Authorization", "Bearer $accessToken") .addHeader("Content-Type", "application/json") .post("{}".toRequestBody(jsonMedia)) .build() /** codebuddy.cn 官方 CLI 插件模型(桌面/CLI accessToken 兜底)。 */ - private fun codebuddyRequest(accessToken: String): Request = + private fun codebuddyRequest(credential: Credential.WorkBuddyCredential, accessToken: String): Request = Request.Builder() - .url("https://www.codebuddy.cn/v2/billing/meter/get-user-resource") + .url(credential.resourceUrl()) .addHeader("Authorization", "Bearer $accessToken") .addHeader("Content-Type", "application/json") .post(codebuddyBody.toRequestBody(jsonMedia)) @@ -208,9 +228,9 @@ class WorkBuddyRepository constructor( * refresh_token 单次轮换:POST token/refresh(X-Refresh-Token 头)。 * 只读取并返回新凭据;轮换后的提交交给主流程的 save(同一成功才落库)。 */ - private fun tryRefesh(refreshToken: String): RefreshedWorkBuddy? { + private fun tryRefesh(credential: Credential.WorkBuddyCredential, refreshToken: String): RefreshedWorkBuddy? { val request = Request.Builder() - .url("https://copilot.tencent.com/v2/plugin/auth/token/refresh") + .url(credential.refreshUrl()) .addHeader("X-Refresh-Token", refreshToken) .post("{}".toRequestBody(jsonMedia)) .build() @@ -595,7 +615,7 @@ private fun parseResourceDetail(body: String): Map? { var rotated: Credential.WorkBuddyCredential? = null repeat(2) { attempt -> val request = Request.Builder() - .url("https://www.codebuddy.cn" + path) + .url(credential.originBase() + path) .addHeader("Authorization", "Bearer " + access) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") @@ -615,7 +635,7 @@ private fun parseResourceDetail(body: String): Map? { when { code in 200..299 -> return AuthedPostResult.Ok(body, rotated) (code == 401 || code == 403) && attempt == 0 -> { - val refreshed = tryRefesh(refresh) ?: return AuthedPostResult.Failed(rotated) + val refreshed = tryRefesh(credential, refresh) ?: return AuthedPostResult.Failed(rotated) val expiresAt = if (refreshed.expiresIn > 0) { System.currentTimeMillis() + refreshed.expiresIn * 1000 } else {