refactor(wechat_woc): 优化WocBridgeClient HTTP请求逻辑,减少凭证查询次数并调整重试策略

- 新增_resolve_endpoint方法一次性解析bridge URL与鉴权token,避免重复调用_get_credentials
- 重构_execute_http,替换原有的URL构造与token获取逻辑,统一通过新方法处理
- 调整重试策略:仅对瞬时网络错误做1次快速重试,移除超时与5xx错误的重试,避免双重重试放大延迟
- 更新相关注释与常量命名,对齐现有逻辑说明
This commit is contained in:
Kris 2026-07-08 00:18:55 +08:00
parent 76981cd9bc
commit 17abcd4502

View File

@ -433,6 +433,51 @@ class WocBridgeClient:
# 通用 HTTP 执行
# ------------------------------------------------------------------
async def _resolve_endpoint(
self,
account_id: str,
path: str,
override_bridge_url: str | None,
bridge_token: str | None,
) -> tuple[str, str | None]:
"""一次性解析 bridge URL 与鉴权 token避免双重凭证缓存查询。
wizard 模式``override_bridge_url`` 为空时从 CachePort 凭证缓存
读取一次凭证字典同时提取 ``bridge_url`` ``bridge_token``避免
``_build_bridge_url`` ``_get_bridge_token`` 各调一次 ``_get_credentials``
导致同一份凭证查 2 wizard 模式下直接使用调用方传入的
``override_bridge_url````bridge_token`` 透传
@pre
- ``account_id`` 非空
- ``path`` ``/`` 开头
- ``override_bridge_url`` 非空时必须以 ``http://`` ``https://`` 开头
@post
- 返回 ``(url, bridge_token_or_none)`` 元组
@failure
- 透传 ``_get_credentials`` ``DependencyError``
- ``DependencyError``bridge_url 为空字符串
"""
if override_bridge_url is not None:
base = override_bridge_url.rstrip("/")
url = f"{base}{path}" if path.startswith("/") else f"{base}/{path}"
return url, bridge_token
# 非 wizard 模式:一次读取凭证字典,同时提取 bridge_url 与 bridge_token
credentials = await self._get_credentials(account_id)
bridge_url = credentials.get("bridge_url", "").rstrip("/")
if not bridge_url:
raise DependencyError(
dep=WOC_BRIDGE_DEP,
cause=Exception(f"bridge_url_empty: account_id={account_id}"),
)
if not path.startswith("/"):
path = f"/{path}"
# 调用方未显式传入 bridge_token 时从凭证读取;传入时优先使用调用方值
resolved_token = bridge_token if bridge_token is not None else credentials.get("bridge_token", "")
return f"{bridge_url}{path}", resolved_token
async def _execute_http(
self,
method: str,
@ -454,19 +499,15 @@ class WocBridgeClient:
在途请求计数进入时 +1退出时 -1归零时设置 ``_drain_event``
lifecycle ``drain`` 等待
URL 构造默认通过 ``_build_bridge_url(account_id, path)`` CachePort
凭证缓存读取 ``bridge_url`` 拼接 ``override_bridge_url`` 非空时直接与
``path`` 拼接绕过缓存读取用于 wizard 阶段账户未创建时探活对齐
wechat_ilink ``getconfig_with_token`` 模式
URL 构造与鉴权注入通过 ``_resolve_endpoint`` 一次性从凭证缓存读取
``bridge_url`` ``bridge_token``避免双重缓存查询非空 token 注入
``Authorization: Bearer <token>`` ``override_bridge_url`` 非空时
绕过缓存读取wizard 阶段账户未创建时探活
鉴权注入``bridge_token`` 非空时注入 ``Authorization: Bearer <token>``
用于通过 WechatOnCloud 面板 ``/api/bridge/:id/*`` 反代路由的 M2M
鉴权与面板管理员会话二选一``bridge_token`` ``None`` 或空字符串
时不注入该头回退到面板会话鉴权兼容未启用 M2M 鉴权的旧部署
重试策略``httpx.TimeoutException`` / ``httpx.NetworkError`` / 5xx
``HTTPStatusError`` 最多重试 ``_HTTP_RETRY_ATTEMPTS`` 指数退避
4xx 429不重试由调用方或 outbox 决策P2-3 超时重试
重试策略仅对 ``httpx.NetworkError``连接失败/DNS 失败等瞬时网络抖动
1 次快速重试不等待超时与 5xx 不重试由调用方或 outbox 机制
决策P2-3 超时重试避免 bridge 客户端重试与 outbox 退避重试双重
放大延迟
@pre
- ``method`` 为合法 HTTP 方法GET / POST
@ -493,16 +534,10 @@ class WocBridgeClient:
)
# 生成请求级 trace_id注入 X-Trace-Id 头与 error_translator 供异常追踪
trace_id = uuid4().hex
if override_bridge_url is not None:
base = override_bridge_url.rstrip("/")
url = f"{base}{path}" if path.startswith("/") else f"{base}/{path}"
else:
url = await self._build_bridge_url(account_id, path)
# 鉴权 token 统一获取:非 wizard 模式override_bridge_url 为空)且
# 调用方未显式传入 bridge_token 时从凭证缓存读取。wizard 模式下
# bridge_token 由调用方显式传入(可为 None表示未配置 M2M 鉴权)。
if bridge_token is None and override_bridge_url is None:
bridge_token = await self._get_bridge_token(account_id)
# 一次性解析 URL 与鉴权 token避免双重凭证缓存查询
url, resolved_token = await self._resolve_endpoint(
account_id, path, override_bridge_url, bridge_token
)
# 合并请求头:调用方显式传入的 headers 优先(覆盖 User-Agent / X-Trace-Id
request_headers: dict[str, str] = {
"User-Agent": self._user_agent,
@ -510,11 +545,11 @@ class WocBridgeClient:
}
if json_body is not None:
request_headers["Content-Type"] = "application/json"
if bridge_token:
if resolved_token:
# 注入 M2M 鉴权头,通过 WechatOnCloud 面板反代路由的双鉴权校验。
# bridge_token 为 None 或空字符串时不注入,回退到面板会话鉴权
# resolved_token 为 None 或空字符串时不注入,回退到面板会话鉴权
# (兼容未启用 M2M 鉴权的旧部署)。
request_headers["Authorization"] = f"Bearer {bridge_token}"
request_headers["Authorization"] = f"Bearer {resolved_token}"
if headers is not None:
request_headers.update(headers)
# 使用 path 作为 operation 上下文,供 translate_call_error 丰富超时错误信息
@ -523,7 +558,7 @@ class WocBridgeClient:
self._drain_event.clear()
last_exc: httpx.HTTPError | None = None
try:
for attempt in range(1, _HTTP_RETRY_ATTEMPTS + 1):
for attempt in range(1, _HTTP_NETWORK_ERROR_MAX_ATTEMPTS + 1):
# 每次重试前重新检查活跃状态stop/pause 后立即拒绝新请求
if not self._active:
raise DependencyError(
@ -554,26 +589,12 @@ class WocBridgeClient:
resp.raise_for_status()
last_exc = None
break
except httpx.TimeoutException as exc:
last_exc = exc
if attempt == _HTTP_RETRY_ATTEMPTS:
break
await self._logger.warn(
"woc bridge HTTP request timeout, retrying",
method=method,
path=path,
attempt=attempt,
trace_id=trace_id,
)
await asyncio.sleep(
min(
_HTTP_RETRY_WAIT_MAX_SECONDS,
_HTTP_RETRY_WAIT_MIN_SECONDS * (2 ** (attempt - 1)),
)
)
except httpx.NetworkError as exc:
# 仅对网络错误(连接失败/DNS 失败等瞬时抖动)做 1 次快速重试,
# 不等待。超时与 5xx 不重试,交给 outbox 退避重试机制处理,
# 避免双重重试放大延迟。
last_exc = exc
if attempt == _HTTP_RETRY_ATTEMPTS:
if attempt == _HTTP_NETWORK_ERROR_MAX_ATTEMPTS:
break
await self._logger.warn(
"woc bridge HTTP request network error, retrying",
@ -582,33 +603,14 @@ class WocBridgeClient:
attempt=attempt,
trace_id=trace_id,
)
await asyncio.sleep(
min(
_HTTP_RETRY_WAIT_MAX_SECONDS,
_HTTP_RETRY_WAIT_MIN_SECONDS * (2 ** (attempt - 1)),
)
)
except httpx.HTTPStatusError as exc:
if exc.response.status_code < 500:
# 4xx含 429为客户端/限流错误,不重试
raise
# 5xx 服务端错误不重试,交给 outbox 退避重试机制处理
last_exc = exc
if attempt == _HTTP_RETRY_ATTEMPTS:
break
await self._logger.warn(
"woc bridge HTTP request server error, retrying",
method=method,
path=path,
attempt=attempt,
status=exc.response.status_code,
trace_id=trace_id,
)
await asyncio.sleep(
min(
_HTTP_RETRY_WAIT_MAX_SECONDS,
_HTTP_RETRY_WAIT_MIN_SECONDS * (2 ** (attempt - 1)),
)
)
break
# httpx.TimeoutException 不重试,交给 outbox 退避重试机制处理
if last_exc is not None:
raise last_exc
except httpx.HTTPError as exc:
@ -1030,8 +1032,10 @@ class WocBridgeClient:
cause=Exception("client_inactive_after_stop"),
)
trace_id = uuid4().hex
bridge_url = await self._build_bridge_url(account_id, "/api/messages/stream")
bridge_token = await self._get_bridge_token(account_id)
# 一次性解析 URL 与鉴权 token避免双重凭证缓存查询
bridge_url, bridge_token = await self._resolve_endpoint(
account_id, "/api/messages/stream", None, None
)
request_headers: dict[str, str] = {
"User-Agent": self._user_agent,
"X-Trace-Id": trace_id,