kevin.yang 1 ngày trước cách đây
mục cha
commit
5eda1ee301

+ 31 - 10
agent/tools/docker_runner.py

@@ -259,17 +259,38 @@ class DockerWorkspaceClient:
                 parts.append(chunk.encode("latin-1"))
             else:
                 parts.append(bytes(chunk))
-        bio = io.BytesIO(b"".join(parts))
-        with tarfile.open(fileobj=bio, mode="r") as tar:
-            member = tar.next()
-            if member is None:
-                return b""
-            if member.isdir():
+        raw = b"".join(parts)
+
+        if raw:
+            try:
+                bio = io.BytesIO(raw)
+                with tarfile.open(fileobj=bio, mode="r") as tar:
+                    member = tar.next()
+                    if member is None:
+                        raise tarfile.ReadError("empty archive")
+                    if member.isdir():
+                        raise IsADirectoryError(container_path)
+                    ef = tar.extractfile(member)
+                    if ef is None:
+                        return b""
+                    return ef.read()
+            except tarfile.ReadError as e:
+                logger.debug(
+                    "get_archive tar parse failed path=%s nbytes=%s: %s",
+                    container_path,
+                    len(raw),
+                    e,
+                )
+
+        code, out, err = self.sync_exec_argv(["cat", "--", container_path], workdir="/")
+        err_t = err.decode("utf-8", errors="replace") if err else ""
+        if code != 0:
+            if "Is a directory" in err_t:
                 raise IsADirectoryError(container_path)
-            ef = tar.extractfile(member)
-            if ef is None:
-                return b""
-            return ef.read()
+            if code in (1, 127) or "No such file" in err_t or "Cannot open" in err_t:
+                raise FileNotFoundError(container_path)
+            raise RuntimeError(f"read_file fallback cat failed: {err_t[:500]}")
+        return out
 
     @staticmethod
     def _posixpath_dir(p: str) -> str:

+ 1 - 1
gateway/core/channels/feishu/bridge.py

@@ -577,7 +577,7 @@ class FeishuHttpRunApiExecutor:
         workspace_prefix: str = "feishu",
         channel_id: str = "feishu",
         lifecycle_trace_backend: LifecycleTraceBackend | None = None,
-        stop_container_on_trace_terminal: bool = True,
+        stop_container_on_trace_terminal: bool = False,
         stop_container_on_trace_not_found: bool = True,
         release_ref_on_trace_terminal: bool = False,
     ) -> None:

+ 10 - 3
gateway/core/channels/feishu/manager.py

@@ -40,9 +40,12 @@ class FeishuChannelConfig:
     typing_reaction_enabled: bool = True
     typing_reaction_emoji: str = "Typing"
     # Trace 跟单结束后的生命周期(Workspace 沙箱 / 渠道绑定)
-    stop_container_on_trace_terminal: bool = True
+    # 默认不在单次 Trace 终态时停沙箱:同一用户共用一个 workspace/容器,多轮对话与多 trace 复用。
+    stop_container_on_trace_terminal: bool = False
     stop_container_on_trace_not_found: bool = True
     release_ref_on_trace_terminal: bool = False
+    # False:绑定新 trace 时仍保留旧 trace 在 workspace 的引用,便于 Executor 等按 trace_id 解析同一用户目录
+    release_previous_trace_ref_on_bind: bool = False
 
 
 class FeishuChannelManager(ChannelRegistry):
@@ -59,7 +62,10 @@ class FeishuChannelManager(ChannelRegistry):
         )
         self._workspace_manager = WorkspaceManager.from_env()
         self._trace_manager = TraceManager.from_env(self._workspace_manager)
-        self._trace_backend = LifecycleTraceBackend(self._trace_manager)
+        self._trace_backend = LifecycleTraceBackend(
+            self._trace_manager,
+            release_previous_trace_ref_on_bind=self._config.release_previous_trace_ref_on_bind,
+        )
         self._identity = DefaultUserIdentityResolver()
         self._executor = FeishuHttpRunApiExecutor(
             base_url=self._config.agent_api_base_url,
@@ -131,9 +137,10 @@ class FeishuChannelManager(ChannelRegistry):
                 assistant_max_text_chars=env_int("FEISHU_AGENT_ASSISTANT_MAX_CHARS", 8000),
                 typing_reaction_enabled=env_bool("FEISHU_TYPING_REACTION", True),
                 typing_reaction_emoji=env_str("FEISHU_TYPING_REACTION_EMOJI", "Typing") or "Typing",
-                stop_container_on_trace_terminal=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_TERMINAL", True),
+                stop_container_on_trace_terminal=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_TERMINAL", False),
                 stop_container_on_trace_not_found=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_NOT_FOUND", True),
                 release_ref_on_trace_terminal=env_bool("GATEWAY_LIFECYCLE_RELEASE_REF_ON_TRACE_TERMINAL", False),
+                release_previous_trace_ref_on_bind=env_bool("GATEWAY_LIFECYCLE_RELEASE_PREV_TRACE_REF_ON_BIND", False),
             )
         )
 

+ 8 - 2
gateway/core/lifecycle/trace/backend.py

@@ -15,8 +15,14 @@ logger = logging.getLogger(__name__)
 
 
 class LifecycleTraceBackend:
-    def __init__(self, trace_manager: TraceManager) -> None:
+    def __init__(
+        self,
+        trace_manager: TraceManager,
+        *,
+        release_previous_trace_ref_on_bind: bool = False,
+    ) -> None:
         self._tm = trace_manager
+        self._release_previous_trace_ref_on_bind = release_previous_trace_ref_on_bind
         self._lock = asyncio.Lock()
         self._channel_user_trace: dict[tuple[str, str], str] = {}
 
@@ -51,7 +57,7 @@ class LifecycleTraceBackend:
             prev_tid = self._channel_user_trace.get(key)
             if prev_tid == agent_trace_id:
                 return
-        if prev_tid:
+        if prev_tid and self._release_previous_trace_ref_on_bind:
             await self._tm.release_agent_trace(workspace_id, prev_tid)
             logger.info(
                 "Lifecycle: 已解除旧 trace_id=%s workspace_id=%s(将绑定新 trace)",