fix(wb): 余额与 token 刷新按区域分流(INTL 走 workbuddy.ai,CN 保持 workbuddy.cn/copilot.tencent.com)

This commit is contained in:
Liuxinyu176 2026-10-10 01:59:06 +08:00
parent 1dd72986f5
commit 809b8db2d3

View File

@ -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<String, String>? {
private fun fetchResourceDetailExtras(credential: Credential.WorkBuddyCredential, accessToken: String): Map<String, String>? {
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<String, String>? {
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<String, String>? {
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 {