mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat: 实现跨 API 格式自动转换功能
- 新增端点级 format_acceptance_config 配置,控制是否接受跨格式请求 - 重构 EndpointFormDialog 为卡片式布局,支持内联编辑和格式转换开关 - StreamProcessor 实现流式响应的跨格式转换,支持 OpenAI/Claude/Gemini 互转 - CacheAwareScheduler 按端点格式筛选候选,同格式优先于跨格式 - 健康度/熔断按 Provider 端点格式分桶,而非客户端请求格式 - 新增 format_conversion_total 和 format_conversion_duration_seconds 指标 - 新增全局配置 format_conversion_enabled 控制总开关 - Input 组件新增 size="sm" 尺寸选项
This commit is contained in:
@@ -248,6 +248,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
self,
|
||||
source_model: str,
|
||||
provider_id: str,
|
||||
api_format: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
获取模型映射后的实际模型名
|
||||
@@ -255,6 +256,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
Args:
|
||||
source_model: 用户请求的模型名
|
||||
provider_id: Provider ID
|
||||
api_format: Provider 侧 API 格式(用于过滤映射作用域,默认使用 handler FORMAT_ID)
|
||||
|
||||
Returns:
|
||||
映射后的 provider_model_name,没有映射则返回 None
|
||||
@@ -269,8 +271,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
# 传入 api_key.id 作为 affinity_key,实现相同用户稳定选择同一映射
|
||||
# 传入 api_format 用于过滤适用的映射作用域
|
||||
affinity_key = self.api_key.id if self.api_key else None
|
||||
effective_format = api_format or self.FORMAT_ID
|
||||
mapped_name = mapping.model.select_provider_model_name(
|
||||
affinity_key, api_format=self.FORMAT_ID
|
||||
affinity_key, api_format=effective_format
|
||||
)
|
||||
logger.debug(f"[Chat] 模型映射: {source_model} -> {mapped_name}")
|
||||
return mapped_name
|
||||
@@ -297,6 +300,8 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
|
||||
# 创建类型安全的流式上下文
|
||||
ctx = StreamContext(model=model, api_format=api_format)
|
||||
ctx.request_id = self.request_id
|
||||
ctx.client_api_format = api_format.value if hasattr(api_format, "value") else str(api_format)
|
||||
|
||||
# 创建更新状态的回调闭包(可以访问 ctx)
|
||||
def update_streaming_status() -> None:
|
||||
@@ -430,12 +435,24 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
provider_api_format=str(endpoint.api_format) if endpoint.api_format else None,
|
||||
)
|
||||
|
||||
# ctx.api_format 是枚举,需要取 value 作为字符串
|
||||
_api_format_str = (
|
||||
ctx.api_format.value if hasattr(ctx.api_format, "value") else str(ctx.api_format)
|
||||
)
|
||||
provider_api_format = ctx.provider_api_format or _api_format_str
|
||||
client_api_format = ctx.client_api_format or _api_format_str
|
||||
needs_conversion = (
|
||||
bool(getattr(candidate, "needs_conversion", False)) if candidate else False
|
||||
)
|
||||
ctx.needs_conversion = needs_conversion
|
||||
|
||||
# 获取模型映射(优先使用映射匹配到的模型,其次是 Provider 级别的映射)
|
||||
mapped_model = candidate.mapping_matched_model if candidate else None
|
||||
if not mapped_model:
|
||||
mapped_model = await self._get_mapped_model(
|
||||
source_model=ctx.model,
|
||||
provider_id=str(provider.id),
|
||||
api_format=provider_api_format,
|
||||
)
|
||||
|
||||
# 应用模型映射到请求体
|
||||
@@ -445,8 +462,18 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
else:
|
||||
request_body = dict(original_request_body)
|
||||
|
||||
# 准备发送给 Provider 的请求体
|
||||
request_body = self.prepare_provider_request_body(request_body)
|
||||
# 跨格式:先做请求体转换(严格模式,失败触发 failover)
|
||||
if needs_conversion:
|
||||
from src.core.api_format import converter_registry
|
||||
|
||||
request_body = converter_registry.convert_request_strict(
|
||||
request_body,
|
||||
str(client_api_format),
|
||||
str(provider_api_format),
|
||||
)
|
||||
else:
|
||||
# 同格式:按原逻辑做轻量清理(子类可覆盖以移除不需要的字段)
|
||||
request_body = self.prepare_provider_request_body(request_body)
|
||||
|
||||
# 构建请求
|
||||
provider_payload, provider_headers = self._request_builder.build(
|
||||
@@ -663,6 +690,12 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
nonlocal provider_request_headers, provider_request_body, mapped_model_result
|
||||
|
||||
provider_name = str(provider.name)
|
||||
provider_api_format = str(endpoint.api_format or api_format)
|
||||
# 客户端格式(与流式处理保持一致的命名)
|
||||
client_api_format = (
|
||||
api_format.value if hasattr(api_format, "value") else str(api_format)
|
||||
)
|
||||
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
|
||||
|
||||
# 获取模型映射(优先使用映射匹配到的模型,其次是 Provider 级别的映射)
|
||||
mapped_model = candidate.mapping_matched_model if candidate else None
|
||||
@@ -670,6 +703,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
mapped_model = await self._get_mapped_model(
|
||||
source_model=model,
|
||||
provider_id=str(provider.id),
|
||||
api_format=provider_api_format,
|
||||
)
|
||||
|
||||
# 应用模型映射
|
||||
@@ -679,8 +713,18 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
else:
|
||||
request_body = dict(original_request_body)
|
||||
|
||||
# 准备发送给 Provider 的请求体(子类可覆盖以移除不需要的字段)
|
||||
request_body = self.prepare_provider_request_body(request_body)
|
||||
# 跨格式:先做请求体转换(严格模式,失败触发 failover)
|
||||
if needs_conversion:
|
||||
from src.core.api_format import converter_registry
|
||||
|
||||
request_body = converter_registry.convert_request_strict(
|
||||
request_body,
|
||||
client_api_format,
|
||||
provider_api_format,
|
||||
)
|
||||
else:
|
||||
# 同格式:按原逻辑做轻量清理(子类可覆盖以移除不需要的字段)
|
||||
request_body = self.prepare_provider_request_body(request_body)
|
||||
|
||||
# 构建请求
|
||||
provider_payload, provider_hdrs = self._request_builder.build(
|
||||
@@ -789,9 +833,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
raw_content = repr(resp.content[:500]) if resp.content else "(empty)"
|
||||
except Exception:
|
||||
raw_content = "(unable to read)"
|
||||
logger.error(
|
||||
f"[{self.request_id}] 无法解析响应 JSON: {e}, 原始内容: {raw_content}"
|
||||
)
|
||||
logger.error(f"[{self.request_id}] 无法解析响应 JSON: {e}, 原始内容: {raw_content}")
|
||||
# 判断错误类型,生成友好的客户端错误消息(不暴露提供商信息)
|
||||
if raw_content == "(empty)" or not raw_content.strip():
|
||||
client_message = "上游服务返回了空响应"
|
||||
@@ -808,7 +850,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
|
||||
# 检查响应体中的嵌套错误(HTTP 200 但响应体包含错误)
|
||||
if isinstance(response_json, dict):
|
||||
parser = get_parser_for_format(api_format)
|
||||
parser = get_parser_for_format(provider_api_format)
|
||||
if parser.is_error_response(response_json):
|
||||
parsed = parser.parse_response(response_json, 200)
|
||||
logger.warning(
|
||||
@@ -825,6 +867,16 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
error_status=parsed.error_type,
|
||||
)
|
||||
|
||||
# 跨格式:响应转换回 client_format(严格模式,失败触发 failover)
|
||||
if needs_conversion and isinstance(response_json, dict):
|
||||
from src.core.api_format import converter_registry
|
||||
|
||||
response_json = converter_registry.convert_response_strict(
|
||||
response_json,
|
||||
provider_api_format,
|
||||
str(api_format),
|
||||
)
|
||||
|
||||
return response_json if isinstance(response_json, dict) else {}
|
||||
|
||||
try:
|
||||
|
||||
@@ -76,6 +76,7 @@ class StreamContext:
|
||||
|
||||
# 格式转换信息(CLI handler 需要)
|
||||
client_api_format: str = ""
|
||||
needs_conversion: bool = False # 是否需要跨格式转换(由 handler 层设置)
|
||||
|
||||
# Provider 响应元数据(CLI handler 需要)
|
||||
response_metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -118,6 +119,7 @@ class StreamContext:
|
||||
self.final_usage = None
|
||||
self.final_response = None
|
||||
self.stream_conversion_state = None
|
||||
self.needs_conversion = False
|
||||
|
||||
@property
|
||||
def collected_text(self) -> str:
|
||||
|
||||
@@ -328,14 +328,12 @@ class StreamProcessor:
|
||||
raise
|
||||
except (OSError, IOError) as e:
|
||||
# 网络 I/O 异常:记录警告,可能需要重试
|
||||
logger.warning(
|
||||
f" [{self.request_id}] 预读流时发生网络异常: {type(e).__name__}: {e}"
|
||||
)
|
||||
logger.warning(f" [{self.request_id}] 预读流时发生网络异常: {type(e).__name__}: {e}")
|
||||
except Exception as e:
|
||||
# 未预期的严重异常:记录错误并重新抛出,避免掩盖问题
|
||||
logger.error(
|
||||
f" [{self.request_id}] 预读流时发生严重异常: {type(e).__name__}: {e}",
|
||||
exc_info=True
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -374,19 +372,269 @@ class StreamProcessor:
|
||||
# 使用增量解码器处理跨 chunk 的 UTF-8 字符
|
||||
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
||||
|
||||
# 处理预读数据
|
||||
if prefetched_chunks:
|
||||
for chunk in prefetched_chunks:
|
||||
# 记录首字时间 (TTFB) - 在 yield 之前记录
|
||||
if start_time is not None:
|
||||
ctx.record_first_byte_time(start_time)
|
||||
start_time = None # 只记录一次
|
||||
# 首次输出前触发 streaming 回调(确保 TTFB 已写入 ctx)
|
||||
if not streaming_started and self.on_streaming_start:
|
||||
self.on_streaming_start()
|
||||
streaming_started = True
|
||||
# ctx.api_format 可能是 APIFormat 枚举,需要取 value
|
||||
_api_format_str = (
|
||||
ctx.api_format.value
|
||||
if hasattr(ctx.api_format, "value")
|
||||
else str(ctx.api_format or "")
|
||||
)
|
||||
client_format = (ctx.client_api_format or _api_format_str).upper()
|
||||
provider_format = (ctx.provider_api_format or _api_format_str).upper()
|
||||
# 使用 handler 层预计算的 needs_conversion(由 candidate 决定)
|
||||
needs_conversion = ctx.needs_conversion
|
||||
|
||||
# 把原始数据转发给客户端
|
||||
# 安全检查:needs_conversion 为 True 时,provider_format 必须有值
|
||||
if needs_conversion and not provider_format:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] needs_conversion=True 但 provider_format 为空,回退到透传模式"
|
||||
)
|
||||
needs_conversion = False
|
||||
|
||||
def _mark_stream_started() -> None:
|
||||
nonlocal start_time, streaming_started
|
||||
# 记录首字时间 (TTFB) - 在 yield 之前记录
|
||||
if start_time is not None:
|
||||
ctx.record_first_byte_time(start_time)
|
||||
start_time = None # 只记录一次
|
||||
# 首次输出前触发 streaming 回调(确保 TTFB 已写入 ctx)
|
||||
if not streaming_started and self.on_streaming_start:
|
||||
self.on_streaming_start()
|
||||
streaming_started = True
|
||||
|
||||
def _build_stream_error_payload(message: str) -> dict:
|
||||
if client_format.startswith("OPENAI"):
|
||||
return {
|
||||
"error": {
|
||||
"message": message,
|
||||
"type": "format_conversion_error",
|
||||
}
|
||||
}
|
||||
# Claude 及其他格式使用统一的错误结构
|
||||
return {
|
||||
"type": "error",
|
||||
"error": {
|
||||
"type": "format_conversion_error",
|
||||
"message": message,
|
||||
},
|
||||
}
|
||||
|
||||
# 处理预读数据
|
||||
if needs_conversion:
|
||||
# 延迟导入:仅在需要转换时加载转换器模块
|
||||
from src.core.api_format import (
|
||||
GeminiStreamConversionState,
|
||||
StreamConversionState,
|
||||
converter_registry,
|
||||
)
|
||||
|
||||
# 初始化流式转换状态(首次使用时,根据 Provider 格式选择状态类)
|
||||
if ctx.stream_conversion_state is None:
|
||||
if provider_format == "GEMINI":
|
||||
ctx.stream_conversion_state = GeminiStreamConversionState(
|
||||
model=ctx.mapped_model or ctx.model or "",
|
||||
message_id=ctx.response_id or ctx.request_id or "",
|
||||
)
|
||||
else:
|
||||
ctx.stream_conversion_state = StreamConversionState(
|
||||
model=ctx.mapped_model or ctx.model or "",
|
||||
message_id=ctx.response_id or ctx.request_id or "",
|
||||
)
|
||||
|
||||
skip_next_blank_line = False
|
||||
empty_yield_count = 0 # 空转计数(防护异常情况)
|
||||
|
||||
def _emit_converted_line(normalized_line: str) -> list[bytes]:
|
||||
nonlocal skip_next_blank_line
|
||||
|
||||
# 空行:事件分隔符(避免重复输出)
|
||||
if normalized_line == "":
|
||||
if skip_next_blank_line:
|
||||
skip_next_blank_line = False
|
||||
return []
|
||||
return [b"\n"]
|
||||
|
||||
# 丢弃 Provider 的 event 行,避免泄漏/污染目标格式
|
||||
if normalized_line.startswith("event:"):
|
||||
return []
|
||||
|
||||
# OpenAI done 信号
|
||||
if (
|
||||
normalized_line.startswith("data:")
|
||||
and normalized_line[5:].strip() == "[DONE]"
|
||||
):
|
||||
skip_next_blank_line = True
|
||||
if client_format.startswith("OPENAI"):
|
||||
return [b"data: [DONE]\n\n"]
|
||||
return []
|
||||
|
||||
# 非 data 行:在跨格式场景下统一丢弃(避免泄露 Provider 格式细节)
|
||||
if not normalized_line.startswith("data:"):
|
||||
return []
|
||||
|
||||
data_content = normalized_line[5:].strip()
|
||||
# Gemini 可能包含 JSON 数组包装符,直接忽略
|
||||
if data_content in ("", "[", "]", ","):
|
||||
return []
|
||||
|
||||
try:
|
||||
data_obj = json.loads(data_content)
|
||||
except json.JSONDecodeError:
|
||||
# 跨格式转换时,JSON 解析失败应跳过而不是透传(避免泄漏 Provider 格式)
|
||||
logger.warning(
|
||||
f"[{self.request_id}] JSON 解析失败,跳过该行: {data_content[:100]}"
|
||||
)
|
||||
return []
|
||||
|
||||
if not isinstance(data_obj, dict):
|
||||
return []
|
||||
|
||||
try:
|
||||
converted_events = converter_registry.convert_stream_chunk_strict(
|
||||
data_obj,
|
||||
provider_format,
|
||||
client_format,
|
||||
state=ctx.stream_conversion_state,
|
||||
)
|
||||
except Exception as conv_err:
|
||||
# 首字节后无法 failover:输出目标格式错误事件并终止流
|
||||
# 使用 502 表示上游返回了非预期格式(Bad Gateway)
|
||||
ctx.status_code = 502
|
||||
ctx.error_message = "format_conversion_failed"
|
||||
# 日志记录完整错误(内部排查),客户端只返回脱敏消息
|
||||
logger.warning(f"[{self.request_id}] 流式格式转换失败: {conv_err}")
|
||||
payload = _build_stream_error_payload("响应格式转换失败,请稍后重试")
|
||||
error_bytes = f"data: {json.dumps(payload, ensure_ascii=False)}\n\n".encode(
|
||||
"utf-8"
|
||||
)
|
||||
done_bytes = (
|
||||
b"data: [DONE]\n\n" if client_format.startswith("OPENAI") else b""
|
||||
)
|
||||
return [error_bytes, done_bytes]
|
||||
|
||||
skip_next_blank_line = True
|
||||
out: list[bytes] = []
|
||||
for evt in converted_events:
|
||||
out.append(
|
||||
f"data: {json.dumps(evt, ensure_ascii=False)}\n\n".encode("utf-8")
|
||||
)
|
||||
return out
|
||||
|
||||
# 统一处理 prefetched + iterator
|
||||
if prefetched_chunks:
|
||||
for chunk in prefetched_chunks:
|
||||
buffer += chunk
|
||||
while b"\n" in buffer:
|
||||
line_bytes, buffer = buffer.split(b"\n", 1)
|
||||
try:
|
||||
line = decoder.decode(line_bytes + b"\n", False)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] UTF-8 解码失败: {e}, bytes={line_bytes[:50]!r}"
|
||||
)
|
||||
line = ""
|
||||
|
||||
if line:
|
||||
self._process_line(ctx, sse_parser, line)
|
||||
normalized_line = line.rstrip("\r\n") if line else ""
|
||||
out_chunks = _emit_converted_line(normalized_line)
|
||||
if not out_chunks:
|
||||
empty_yield_count += 1
|
||||
if empty_yield_count == StreamDefaults.MAX_EMPTY_YIELDS_WARNING:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] 流式转换连续 {empty_yield_count} 次空产出"
|
||||
)
|
||||
else:
|
||||
empty_yield_count = 0
|
||||
for out in out_chunks:
|
||||
if not out:
|
||||
continue
|
||||
_mark_stream_started()
|
||||
yield out
|
||||
# 转换失败:已输出 error(可能还包含 done),直接终止
|
||||
if ctx.error_message == "format_conversion_failed":
|
||||
return
|
||||
|
||||
async for chunk in byte_iterator:
|
||||
buffer += chunk
|
||||
while b"\n" in buffer:
|
||||
line_bytes, buffer = buffer.split(b"\n", 1)
|
||||
try:
|
||||
line = decoder.decode(line_bytes + b"\n", False)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] UTF-8 解码失败: {e}, bytes={line_bytes[:50]!r}"
|
||||
)
|
||||
line = ""
|
||||
|
||||
if line:
|
||||
self._process_line(ctx, sse_parser, line)
|
||||
normalized_line = line.rstrip("\r\n") if line else ""
|
||||
out_chunks = _emit_converted_line(normalized_line)
|
||||
if not out_chunks:
|
||||
empty_yield_count += 1
|
||||
if empty_yield_count == StreamDefaults.MAX_EMPTY_YIELDS_WARNING:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] 流式转换连续 {empty_yield_count} 次空产出"
|
||||
)
|
||||
else:
|
||||
empty_yield_count = 0
|
||||
for out in out_chunks:
|
||||
if not out:
|
||||
continue
|
||||
_mark_stream_started()
|
||||
yield out
|
||||
if ctx.error_message == "format_conversion_failed":
|
||||
return
|
||||
|
||||
# 处理剩余缓冲区(needs_conversion 分支内,可复用 _emit_converted_line)
|
||||
if buffer:
|
||||
try:
|
||||
line = decoder.decode(buffer, True)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] 处理剩余缓冲区失败: {e}, bytes={buffer[:50]!r}"
|
||||
)
|
||||
line = ""
|
||||
if line:
|
||||
self._process_line(ctx, sse_parser, line)
|
||||
normalized_line = line.rstrip("\r\n")
|
||||
out_chunks = _emit_converted_line(normalized_line)
|
||||
for out in out_chunks:
|
||||
if out:
|
||||
_mark_stream_started()
|
||||
yield out
|
||||
# 转换失败:已输出 error,直接终止
|
||||
if ctx.error_message == "format_conversion_failed":
|
||||
return
|
||||
|
||||
else:
|
||||
if prefetched_chunks:
|
||||
for chunk in prefetched_chunks:
|
||||
_mark_stream_started()
|
||||
yield chunk
|
||||
|
||||
buffer += chunk
|
||||
# 处理缓冲区中的完整行
|
||||
while b"\n" in buffer:
|
||||
line_bytes, buffer = buffer.split(b"\n", 1)
|
||||
try:
|
||||
# 使用增量解码器,可以正确处理跨 chunk 的多字节字符
|
||||
line = decoder.decode(line_bytes + b"\n", False)
|
||||
self._process_line(ctx, sse_parser, line)
|
||||
except Exception as e:
|
||||
# 解码失败,记录警告但继续处理
|
||||
logger.warning(
|
||||
f"[{self.request_id}] UTF-8 解码失败: {e}, "
|
||||
f"bytes={line_bytes[:50]!r}"
|
||||
)
|
||||
continue
|
||||
|
||||
# 处理剩余的流数据
|
||||
if not needs_conversion:
|
||||
async for chunk in byte_iterator:
|
||||
_mark_stream_started()
|
||||
|
||||
# 原始数据透传
|
||||
yield chunk
|
||||
|
||||
buffer += chunk
|
||||
@@ -405,46 +653,15 @@ class StreamProcessor:
|
||||
)
|
||||
continue
|
||||
|
||||
# 处理剩余的流数据
|
||||
async for chunk in byte_iterator:
|
||||
# 记录首字时间 (TTFB) - 在 yield 之前记录(如果预读数据为空)
|
||||
if start_time is not None:
|
||||
ctx.record_first_byte_time(start_time)
|
||||
start_time = None # 只记录一次
|
||||
# 首次输出前触发 streaming 回调(确保 TTFB 已写入 ctx)
|
||||
if not streaming_started and self.on_streaming_start:
|
||||
self.on_streaming_start()
|
||||
streaming_started = True
|
||||
|
||||
# 原始数据透传
|
||||
yield chunk
|
||||
|
||||
buffer += chunk
|
||||
# 处理缓冲区中的完整行
|
||||
while b"\n" in buffer:
|
||||
line_bytes, buffer = buffer.split(b"\n", 1)
|
||||
try:
|
||||
# 使用增量解码器,可以正确处理跨 chunk 的多字节字符
|
||||
line = decoder.decode(line_bytes + b"\n", False)
|
||||
self._process_line(ctx, sse_parser, line)
|
||||
except Exception as e:
|
||||
# 解码失败,记录警告但继续处理
|
||||
logger.warning(
|
||||
f"[{self.request_id}] UTF-8 解码失败: {e}, "
|
||||
f"bytes={line_bytes[:50]!r}"
|
||||
)
|
||||
continue
|
||||
|
||||
# 处理剩余的缓冲区数据(如果有未完成的行)
|
||||
if buffer:
|
||||
# 处理剩余的缓冲区数据(仅非转换分支,转换分支已在内部处理)
|
||||
if not needs_conversion and buffer:
|
||||
try:
|
||||
# 使用 final=True 处理最后的不完整字符
|
||||
line = decoder.decode(buffer, True)
|
||||
self._process_line(ctx, sse_parser, line)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[{self.request_id}] 处理剩余缓冲区失败: {e}, "
|
||||
f"bytes={buffer[:50]!r}"
|
||||
f"[{self.request_id}] 处理剩余缓冲区失败: {e}, bytes={buffer[:50]!r}"
|
||||
)
|
||||
|
||||
# 处理剩余事件
|
||||
|
||||
Reference in New Issue
Block a user