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