Skip to content

Commit 078dbfb

Browse files
committed
feat(cli): support multi-round reply and large response handling
- Fix client to receive large responses by using loop recv instead of single recv(4096) - Save base64 images to temp files instead of exposing in JSON response - Implement adaptive delay mechanism for multi-round reply collection: * First send: 5s delay (fast response for simple text) * Subsequent sends: 10s delay (auto-switch for tool invocation) - Support tool invocation scenarios with multiple replies (text + images) - Add detailed logging for debugging multi-round reply collection
1 parent 56a430f commit 078dbfb

3 files changed

Lines changed: 122 additions & 15 deletions

File tree

astrbot-cli

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,23 @@ def send_message(
100100
request_data = json.dumps(request, ensure_ascii=False).encode("utf-8")
101101
client_socket.sendall(request_data)
102102

103-
# 接收响应
104-
response_data = client_socket.recv(4096)
103+
# 接收响应(循环接收所有数据,支持大响应如base64图片)
104+
response_data = b""
105+
while True:
106+
chunk = client_socket.recv(4096)
107+
if not chunk:
108+
break
109+
response_data += chunk
110+
# 尝试解析JSON,如果成功说明接收完整
111+
try:
112+
response = json.loads(response_data.decode("utf-8"))
113+
return response
114+
except json.JSONDecodeError:
115+
# JSON不完整,继续接收
116+
continue
117+
118+
# 如果循环结束仍未成功解析,尝试最后一次
105119
response = json.loads(response_data.decode("utf-8"))
106-
107120
return response
108121

109122
except TimeoutError:

astrbot/core/platform/sources/cli/cli_adapter.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from astrbot import logger
1616
from astrbot.core.message.components import Plain
17-
from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_temp_path
1817
from astrbot.core.message.message_event_result import MessageChain
1918
from astrbot.core.platform import (
2019
AstrBotMessage,
@@ -24,6 +23,7 @@
2423
PlatformMetadata,
2524
)
2625
from astrbot.core.platform.astr_message_event import MessageSesion
26+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_temp_path
2727

2828
from ...register import register_platform_adapter
2929
from .cli_event import CLIMessageEvent
@@ -36,7 +36,7 @@
3636
"type": "cli",
3737
"enable": False, # 默认关闭,开发时手动启用
3838
"mode": "socket", # 默认使用Socket模式
39-
"socket_path": "/tmp/astrbot.sock",
39+
"socket_path": None, # None表示使用动态路径(temp_dir/astrbot.sock)
4040
"whitelist": [], # 空白名单表示允许所有
4141
"use_isolated_sessions": False, # 是否启用会话隔离(每个请求独立会话)
4242
"session_ttl": 30, # 会话过期时间(秒),仅在use_isolated_sessions=True时生效,测试用30秒,生产建议1800秒(30分钟)
@@ -104,10 +104,12 @@ def __init__(
104104

105105
# 文件I/O配置
106106
self.input_file = platform_config.get(
107-
"input_file", "/tmp/astrbot_cli/input.txt"
107+
"input_file",
108+
os.path.join(get_astrbot_temp_path(), "astrbot_cli", "input.txt"),
108109
)
109110
self.output_file = platform_config.get(
110-
"output_file", "/tmp/astrbot_cli/output.txt"
111+
"output_file",
112+
os.path.join(get_astrbot_temp_path(), "astrbot_cli", "output.txt"),
111113
)
112114
self.poll_interval = platform_config.get("poll_interval", 1.0)
113115

@@ -519,11 +521,39 @@ async def _handle_socket_client(self, client_socket) -> None:
519521
)
520522
image_info["error"] = str(e)
521523
elif comp.file.startswith("base64://"):
522-
image_info["type"] = "base64"
523-
# 返回完整的base64数据
524-
base64_data = comp.file[9:]
525-
image_info["base64_data"] = base64_data
526-
image_info["base64_length"] = len(base64_data)
524+
# 将base64数据保存到临时文件,避免在JSON中暴露大量数据
525+
try:
526+
import base64
527+
import os
528+
import tempfile
529+
530+
base64_data = comp.file[9:]
531+
image_data = base64.b64decode(base64_data)
532+
533+
# 生成临时文件路径
534+
temp_dir = get_astrbot_temp_path()
535+
os.makedirs(temp_dir, exist_ok=True)
536+
temp_file = tempfile.NamedTemporaryFile(
537+
delete=False,
538+
suffix=".png",
539+
dir=temp_dir,
540+
)
541+
temp_file.write(image_data)
542+
temp_file.close()
543+
544+
image_info["type"] = "file"
545+
image_info["path"] = temp_file.name
546+
image_info["size"] = len(image_data)
547+
logger.debug(
548+
f"[PROCESS] Saved base64 image to file: {temp_file.name}, size: {len(image_data)} bytes"
549+
)
550+
except Exception as e:
551+
logger.error(
552+
f"[ERROR] Failed to save base64 image: {e}"
553+
)
554+
image_info["type"] = "base64"
555+
image_info["error"] = str(e)
556+
image_info["base64_length"] = len(base64_data)
527557
images.append(image_info)
528558

529559
# 发送成功响应

astrbot/core/platform/sources/cli/cli_event.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ def __init__(
5353
self.output_queue = output_queue
5454
self.response_future = response_future
5555

56+
# 用于收集多次回复
57+
self.send_buffer = None
58+
self._response_delay_task = None
59+
self._response_delay = 3.0 # 延迟3秒收集所有回复(支持工具调用等多轮场景)
60+
5661
logger.debug("[EXIT] CLIMessageEvent.__init__ return=None")
5762

5863
async def send(self, message_chain: MessageChain) -> dict[str, Any]:
@@ -68,7 +73,7 @@ async def send(self, message_chain: MessageChain) -> dict[str, Any]:
6873
"[ENTRY] CLIMessageEvent.send inputs={message_chain=%s}", message_chain
6974
)
7075

71-
# Socket模式:直接设置Future结果(返回完整MessageChain以支持图片等组件)
76+
# Socket模式:收集多次回复
7277
if self.response_future is not None and not self.response_future.done():
7378
# 预处理本地文件图片:立即读取并转换为base64(避免临时文件被删除)
7479
import base64
@@ -100,8 +105,32 @@ async def send(self, message_chain: MessageChain) -> dict[str, Any]:
100105
f"[ERROR] Failed to read image file {file_path}: {e}"
101106
)
102107

103-
self.response_future.set_result(message_chain)
104-
logger.debug("[PROCESS] Set socket response future with MessageChain")
108+
# 收集多次回复到buffer(自适应延迟机制)
109+
if not self.send_buffer:
110+
# 第一次send:初始化buffer,使用中等延迟(5秒)
111+
# 5秒足够等待工具调用的第二次回复,同时不会让简单回复等太久
112+
self.send_buffer = message_chain
113+
self._response_delay = 5.0
114+
logger.info("[PROCESS] First send: initialized buffer with 5s delay")
115+
else:
116+
# 后续send:追加到buffer,切换到长延迟(10秒)
117+
# 确保能收集到所有工具调用的回复
118+
self.send_buffer.chain.extend(message_chain.chain)
119+
self._response_delay = 10.0
120+
logger.info(
121+
f"[PROCESS] Appended to buffer (switched to 10s delay), total: {len(self.send_buffer.chain)} components"
122+
)
123+
124+
# 取消之前的延迟任务(如果存在)
125+
if self._response_delay_task and not self._response_delay_task.done():
126+
self._response_delay_task.cancel()
127+
logger.info("[PROCESS] Cancelled previous delay task")
128+
129+
# 启动新的延迟任务(每次send都重置延迟)
130+
self._response_delay_task = asyncio.create_task(self._delayed_response())
131+
logger.info(
132+
f"[PROCESS] Started new delay task ({self._response_delay}s)"
133+
)
105134
else:
106135
# 其他模式:将消息放入输出队列
107136
await self.output_queue.put(message_chain)
@@ -129,3 +158,38 @@ async def reply(self, message_chain: MessageChain) -> dict[str, Any]:
129158
logger.debug("[EXIT] CLIMessageEvent.reply return=%s", result)
130159

131160
return result
161+
162+
async def _delayed_response(self) -> None:
163+
"""延迟响应:等待一段时间收集所有回复后统一返回
164+
165+
等待 _response_delay 秒后,将累积的所有消息统一返回给客户端。
166+
这样可以支持插件的多轮回复(如先发文本,再发图片)。
167+
"""
168+
logger.debug(
169+
"[ENTRY] _delayed_response inputs={delay=%s}", self._response_delay
170+
)
171+
172+
try:
173+
# 等待延迟时间,收集所有回复
174+
await asyncio.sleep(self._response_delay)
175+
176+
# 检查 Future 是否还未完成
177+
if self.response_future and not self.response_future.done():
178+
# 将累积的消息设置到 Future
179+
self.response_future.set_result(self.send_buffer)
180+
logger.debug(
181+
"[PROCESS] Set delayed response with %d components",
182+
len(self.send_buffer.chain),
183+
)
184+
else:
185+
logger.warning(
186+
"[WARN] Response future already done or None, skipping set_result"
187+
)
188+
189+
except Exception as e:
190+
logger.error("[ERROR] Failed to set delayed response: %s", e)
191+
# 如果出错,尝试设置异常到 Future
192+
if self.response_future and not self.response_future.done():
193+
self.response_future.set_exception(e)
194+
195+
logger.debug("[EXIT] _delayed_response return=None")

0 commit comments

Comments
 (0)