sycommon-python-lib 0.2.5a29__py3-none-any.whl → 0.2.5a31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,11 @@ def _read_file_safe(path: str) -> bytes:
42
42
  return f.read()
43
43
 
44
44
 
45
+ def _shell_quote(value: str) -> str:
46
+ """对 shell 值加单引号,防注入"""
47
+ return "'" + value.replace("'", "'\\''") + "'"
48
+
49
+
45
50
  def filter_instances_by_owner(instances: list, user_id: str, timeout: float = 3.0) -> list:
46
51
  """过滤掉不属于当前用户的本地沙箱实例。
47
52
 
@@ -120,6 +125,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
120
125
  nacos_service_name: str = None,
121
126
  nacos_group: str = "DEFAULT_GROUP",
122
127
  token: str = None,
128
+ env: dict[str, str] = None,
123
129
  ):
124
130
  """
125
131
  初始化 HTTP 沙箱后端
@@ -132,6 +138,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
132
138
  auto_sync: 是否在首次文件操作时自动同步(默认 False)
133
139
  nacos_service_name: Nacos 服务名(用于故障转移时重新发现服务)
134
140
  nacos_group: Nacos 服务分组
141
+ env: 注入到沙箱的环境变量字典,每次执行命令时自动前置 export
135
142
  """
136
143
  self._base_url = base_url.rstrip("/")
137
144
  self._user_id = user_id.lower().replace(" ", "") if user_id else user_id
@@ -143,7 +150,18 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
143
150
  self._local_mode: Optional[bool] = None
144
151
 
145
152
  # Token authentication: instance-level > env var
146
- self._token = token if token is not None else os.environ.get("SANDBOX_TOKEN", "")
153
+ self._token = token if token is not None else os.environ.get(
154
+ "SANDBOX_TOKEN", "")
155
+
156
+ # 沙箱环境变量注入
157
+ self._env = env or {}
158
+ self._env_prefix = ""
159
+ if self._env:
160
+ exports = " && ".join(
161
+ f"export {k}={_shell_quote(str(v))}"
162
+ for k, v in self._env.items()
163
+ )
164
+ self._env_prefix = f"{exports} && "
147
165
 
148
166
  # Nacos 配置(用于故障转移)
149
167
  self._nacos_service_name = nacos_service_name
@@ -151,7 +169,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
151
169
 
152
170
  # 共享异步 HTTP 会话(连接池复用,惰性初始化)
153
171
  self._async_session: Optional[aiohttp.ClientSession] = None
154
- self._session_loop_id: Optional[int] = None # track which loop created the session
172
+ # track which loop created the session
173
+ self._session_loop_id: Optional[int] = None
155
174
 
156
175
  # 生成 ID
157
176
  self._id = self._base_url.replace(
@@ -224,6 +243,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
224
243
  sync_dirs: List[tuple[str, str]] = None,
225
244
  auto_sync: bool = False,
226
245
  load_balance: bool = True,
246
+ env: dict[str, str] = None,
227
247
  ) -> "HTTPSandboxBackend":
228
248
  """
229
249
  从 Nacos 服务发现创建后端实例
@@ -244,18 +264,21 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
244
264
  Raises:
245
265
  RuntimeError: 无法发现服务实例或 NacosService 未初始化
246
266
  """
247
- SYLogger.info(f"[Sandbox] from_nacos 开始: service_name={service_name}, user_id={user_id}, version={version}")
267
+ SYLogger.info(
268
+ f"[Sandbox] from_nacos 开始: service_name={service_name}, user_id={user_id}, version={version}")
248
269
 
249
270
  # 本地调试支持:读取环境变量 SANDBOX_URL,如果设置则直接使用,跳过 Nacos
250
271
  sandbox_url = os.environ.get('SANDBOX_URL', '').strip()
251
272
  if sandbox_url:
252
- SYLogger.info(f"[Sandbox] 检测到 SANDBOX_URL={sandbox_url},跳过 Nacos 直接连接")
273
+ SYLogger.info(
274
+ f"[Sandbox] 检测到 SANDBOX_URL={sandbox_url},跳过 Nacos 直接连接")
253
275
  return cls(
254
276
  base_url=sandbox_url,
255
277
  user_id=user_id,
256
278
  timeout=timeout,
257
279
  sync_dirs=sync_dirs,
258
280
  auto_sync=auto_sync,
281
+ env=env,
259
282
  )
260
283
 
261
284
  from sycommon.synacos.nacos_service import NacosService
@@ -289,7 +312,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
289
312
  hash_value = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
290
313
  instance_index = hash_value % len(instances)
291
314
  instance = instances[instance_index]
292
- SYLogger.info(f"[Sandbox] 负载均衡: user_id={user_id} -> instance[{instance_index}/{len(instances)}]")
315
+ SYLogger.info(
316
+ f"[Sandbox] 负载均衡: user_id={user_id} -> instance[{instance_index}/{len(instances)}]")
293
317
  else:
294
318
  instance = instances[0]
295
319
 
@@ -305,6 +329,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
305
329
  auto_sync=auto_sync,
306
330
  nacos_service_name=service_name,
307
331
  nacos_group=group,
332
+ env=env,
308
333
  )
309
334
 
310
335
  @classmethod
@@ -318,11 +343,12 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
318
343
  sync_dirs: List[tuple[str, str]] = None,
319
344
  auto_sync: bool = False,
320
345
  load_balance: bool = True,
346
+ env: dict[str, str] = None,
321
347
  ) -> "HTTPSandboxBackend":
322
348
  """异步从 Nacos 服务发现创建后端实例"""
323
349
  return await asyncio.to_thread(
324
350
  cls.from_nacos,
325
- service_name, user_id, group, version, timeout, sync_dirs, auto_sync, load_balance,
351
+ service_name, user_id, group, version, timeout, sync_dirs, auto_sync, load_balance, env,
326
352
  )
327
353
 
328
354
  # ============== 内部方法 - 同步版本 ==============
@@ -351,13 +377,15 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
351
377
  )
352
378
 
353
379
  if not instances:
354
- SYLogger.warning(f"[Sandbox] Nacos 未发现可用服务: {self._nacos_service_name}")
380
+ SYLogger.warning(
381
+ f"[Sandbox] Nacos 未发现可用服务: {self._nacos_service_name}")
355
382
  return False
356
383
 
357
384
  # 过滤掉不属于当前用户的本地沙箱
358
385
  instances = filter_instances_by_owner(instances, self._user_id)
359
386
  if not instances:
360
- SYLogger.warning(f"[Sandbox] 过滤后无可用服务: {self._nacos_service_name}")
387
+ SYLogger.warning(
388
+ f"[Sandbox] 过滤后无可用服务: {self._nacos_service_name}")
361
389
  return False
362
390
 
363
391
  instance = instances[0]
@@ -376,7 +404,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
376
404
  try:
377
405
  sync_result = self._sync_workspace_sync()
378
406
  workspace_files = sync_result.get("files", [])
379
- SYLogger.info(f"[Sandbox] 从旧服务获取 {len(workspace_files)} 个工作空间文件")
407
+ SYLogger.info(
408
+ f"[Sandbox] 从旧服务获取 {len(workspace_files)} 个工作空间文件")
380
409
  except Exception as e:
381
410
  SYLogger.warning(f"[Sandbox] 获取工作空间文件失败: {e}")
382
411
 
@@ -391,9 +420,12 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
391
420
 
392
421
  # 4. 上传工作空间文件到新服务
393
422
  if workspace_files:
394
- SYLogger.info(f"[Sandbox] 上传 {len(workspace_files)} 个工作空间文件到新服务...")
395
- upload_result = self._upload_workspace_files_sync(workspace_files)
396
- SYLogger.info(f"[Sandbox] 工作空间迁移完成: 成功 {upload_result['success']}, 失败 {upload_result['failed']}")
423
+ SYLogger.info(
424
+ f"[Sandbox] 上传 {len(workspace_files)} 个工作空间文件到新服务...")
425
+ upload_result = self._upload_workspace_files_sync(
426
+ workspace_files)
427
+ SYLogger.info(
428
+ f"[Sandbox] 工作空间迁移完成: 成功 {upload_result['success']}, 失败 {upload_result['failed']}")
397
429
 
398
430
  SYLogger.info(f"[Sandbox] 服务切换完成: {old_url} -> {new_url}")
399
431
  return True
@@ -431,11 +463,13 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
431
463
  headers = self._build_headers()
432
464
 
433
465
  url = f"{self._base_url}{endpoint}"
434
- SYLogger.info(f"[Sandbox] POST 请求开始: {url}, timeout={http_timeout}s, command_timeout={actual_timeout}s")
466
+ SYLogger.info(
467
+ f"[Sandbox] POST 请求开始: {url}, timeout={http_timeout}s, command_timeout={actual_timeout}s")
435
468
 
436
469
  for attempt in range(max_retries):
437
470
  try:
438
- SYLogger.info(f"[Sandbox] POST 尝试 {attempt + 1}/{max_retries}: {url}")
471
+ SYLogger.info(
472
+ f"[Sandbox] POST 尝试 {attempt + 1}/{max_retries}: {url}")
439
473
  resp = requests.post(
440
474
  url,
441
475
  json=data,
@@ -444,14 +478,16 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
444
478
  )
445
479
  SYLogger.info(f"[Sandbox] POST 响应状态码: {resp.status_code}")
446
480
  if resp.status_code >= 400:
447
- SYLogger.error(f"[Sandbox] HTTP 错误 {resp.status_code}: {resp.text}")
481
+ SYLogger.error(
482
+ f"[Sandbox] HTTP 错误 {resp.status_code}: {resp.text}")
448
483
  resp.raise_for_status()
449
484
  result = resp.json()
450
485
  SYLogger.info(f"[Sandbox] POST 成功: {url}")
451
486
  return result
452
487
 
453
488
  except (requests.exceptions.RequestException, requests.exceptions.HTTPError) as e:
454
- SYLogger.warning(f"[Sandbox] 请求失败 (尝试 {attempt + 1}/{max_retries}): {self._base_url} - {e}")
489
+ SYLogger.warning(
490
+ f"[Sandbox] 请求失败 (尝试 {attempt + 1}/{max_retries}): {self._base_url} - {e}")
455
491
 
456
492
  if attempt < max_retries - 1:
457
493
  backoff = 2 ** attempt
@@ -481,7 +517,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
481
517
  headers = self._build_headers()
482
518
 
483
519
  url = f"{self._base_url}{endpoint}"
484
- SYLogger.info(f"[Sandbox] Async POST 请求开始: {url}, timeout={http_timeout}s")
520
+ SYLogger.info(
521
+ f"[Sandbox] Async POST 请求开始: {url}, timeout={http_timeout}s")
485
522
 
486
523
  timeout_obj = aiohttp.ClientTimeout(total=http_timeout)
487
524
  session = await self._get_async_session()
@@ -509,7 +546,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
509
546
  try:
510
547
  return await self._post_async(endpoint, data, timeout)
511
548
  except (aiohttp.ClientError, asyncio.TimeoutError) as e:
512
- SYLogger.warning(f"[Sandbox] Async 请求失败 (尝试 {attempt + 1}/{max_retries}): {self._base_url} - {e}")
549
+ SYLogger.warning(
550
+ f"[Sandbox] Async 请求失败 (尝试 {attempt + 1}/{max_retries}): {self._base_url} - {e}")
513
551
 
514
552
  if attempt < max_retries - 1:
515
553
  # 指数退避: 1s, 2s
@@ -553,7 +591,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
553
591
  env_workspace = os.environ.get("SANDBOX_WORKSPACE")
554
592
  if env_workspace:
555
593
  self._workspace_root = env_workspace
556
- SYLogger.info(f"[Sandbox] 使用环境变量 SANDBOX_WORKSPACE={env_workspace}")
594
+ SYLogger.info(
595
+ f"[Sandbox] 使用环境变量 SANDBOX_WORKSPACE={env_workspace}")
557
596
  return self._workspace_root
558
597
 
559
598
  # 从沙箱服务端 health 接口获取
@@ -564,14 +603,17 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
564
603
  workspace = resp.json().get("workspace", "")
565
604
  if workspace:
566
605
  self._workspace_root = workspace
567
- SYLogger.info(f"[Sandbox] 从沙箱服务端获取 WORKSPACE_ROOT={workspace}")
606
+ SYLogger.info(
607
+ f"[Sandbox] 从沙箱服务端获取 WORKSPACE_ROOT={workspace}")
568
608
  return self._workspace_root
569
609
  except Exception as e:
570
610
  SYLogger.warning(f"[Sandbox] 获取沙箱 workspace 失败: {e}")
571
611
 
572
612
  # 兜底:根据平台猜测
573
- self._workspace_root = "/data/sycommon_sandbox" if sys.platform != "darwin" and sys.platform != "win32" else os.path.join(tempfile.gettempdir(), "sycommon_sandbox")
574
- SYLogger.warning(f"[Sandbox] 无法获取沙箱 workspace,使用兜底值: {self._workspace_root}")
613
+ self._workspace_root = "/data/sycommon_sandbox" if sys.platform != "darwin" and sys.platform != "win32" else os.path.join(
614
+ tempfile.gettempdir(), "sycommon_sandbox")
615
+ SYLogger.warning(
616
+ f"[Sandbox] 无法获取沙箱 workspace,使用兜底值: {self._workspace_root}")
575
617
  return self._workspace_root
576
618
 
577
619
  async def _aresolve_workspace_root(self) -> str:
@@ -583,7 +625,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
583
625
  env_workspace = os.environ.get("SANDBOX_WORKSPACE")
584
626
  if env_workspace:
585
627
  self._workspace_root = env_workspace
586
- SYLogger.info(f"[Sandbox] 使用环境变量 SANDBOX_WORKSPACE={env_workspace}")
628
+ SYLogger.info(
629
+ f"[Sandbox] 使用环境变量 SANDBOX_WORKSPACE={env_workspace}")
587
630
  return self._workspace_root
588
631
 
589
632
  # 从沙箱服务端 health 接口获取(使用共享连接池)
@@ -597,20 +640,42 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
597
640
  workspace = data.get("workspace", "")
598
641
  if workspace:
599
642
  self._workspace_root = workspace
600
- SYLogger.info(f"[Sandbox] 从沙箱服务端获取 WORKSPACE_ROOT={workspace}")
643
+ SYLogger.info(
644
+ f"[Sandbox] 从沙箱服务端获取 WORKSPACE_ROOT={workspace}")
601
645
  return self._workspace_root
602
646
  except Exception as e:
603
647
  SYLogger.warning(f"[Sandbox] 异步获取沙箱 workspace 失败: {e}")
604
648
 
605
649
  # 兜底
606
- self._workspace_root = "/data/sycommon_sandbox" if sys.platform != "darwin" and sys.platform != "win32" else os.path.join(tempfile.gettempdir(), "sycommon_sandbox")
607
- SYLogger.warning(f"[Sandbox] 无法获取沙箱 workspace,使用兜底值: {self._workspace_root}")
650
+ self._workspace_root = "/data/sycommon_sandbox" if sys.platform != "darwin" and sys.platform != "win32" else os.path.join(
651
+ tempfile.gettempdir(), "sycommon_sandbox")
652
+ SYLogger.warning(
653
+ f"[Sandbox] 无法获取沙箱 workspace,使用兜底值: {self._workspace_root}")
608
654
  return self._workspace_root
609
655
 
610
656
  # ============== 执行命令 ==============
611
657
 
658
+ def update_env(self, env_updates: dict[str, str]):
659
+ """动态更新沙箱环境变量(合并到现有 env,重建 _env_prefix)
660
+
661
+ 用于运行时刷新 token 等场景,无需重建整个沙箱实例。
662
+ """
663
+ self._env.update(env_updates)
664
+ exports = " && ".join(
665
+ f"export {k}={_shell_quote(str(v))}"
666
+ for k, v in self._env.items()
667
+ )
668
+ self._env_prefix = f"{exports} && " if exports else ""
669
+
670
+ def _inject_env(self, command: str) -> str:
671
+ """在命令前注入环境变量 export"""
672
+ if self._env_prefix:
673
+ return f"{self._env_prefix}{command}"
674
+ return command
675
+
612
676
  def execute(self, command: str, *, timeout: int = None) -> ExecuteResponse:
613
677
  """执行 shell 命令(服务端负责路径隔离和目录初始化)"""
678
+ command = self._inject_env(command)
614
679
  SYLogger.info(f"[Sandbox] execute 开始: command={command}")
615
680
  self._ensure_synced_sync()
616
681
  SYLogger.info(f"[Sandbox] _ensure_synced_sync 完成,开始执行命令: {command}")
@@ -625,7 +690,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
625
690
  exit_code=result["exit_code"],
626
691
  truncated=result.get("truncated", False)
627
692
  )
628
- SYLogger.info(f"[Sandbox] 执行完成: exit_code={response.exit_code}, output={response.output[:500] if response.output else 'None'}...")
693
+ SYLogger.info(
694
+ f"[Sandbox] 执行完成: exit_code={response.exit_code}, output={response.output[:500] if response.output else 'None'}...")
629
695
  return response
630
696
  except Exception as e:
631
697
  SYLogger.error(f"[Sandbox] 执行异常: {e}")
@@ -637,6 +703,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
637
703
 
638
704
  async def aexecute(self, command: str, *, timeout: int = None) -> ExecuteResponse:
639
705
  """异步执行 shell 命令(服务端负责路径隔离和目录初始化)"""
706
+ command = self._inject_env(command)
640
707
  SYLogger.info(f"[Sandbox] aexecute 开始: command={command}")
641
708
  await self._ensure_synced_async()
642
709
  SYLogger.info(f"[Sandbox] _ensure_synced_async 完成,开始执行命令: {command}")
@@ -672,12 +739,14 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
672
739
  SYLogger.info("[Sandbox] 没有配置同步目录")
673
740
  return {}
674
741
 
675
- SYLogger.info(f"[Sandbox] _sync_async 开始, 共 {len(self._sync_dirs)} 个目录")
742
+ SYLogger.info(
743
+ f"[Sandbox] _sync_async 开始, 共 {len(self._sync_dirs)} 个目录")
676
744
  results = {}
677
745
  for idx, entry in enumerate(self._sync_dirs):
678
746
  local_path, remote_path = entry[0], entry[1]
679
747
  mode = entry[2] if len(entry) > 2 else "full"
680
- SYLogger.info(f"[Sandbox] 同步目录 [{idx+1}/{len(self._sync_dirs)}]: {local_path} -> {remote_path} (mode={mode})")
748
+ SYLogger.info(
749
+ f"[Sandbox] 同步目录 [{idx+1}/{len(self._sync_dirs)}]: {local_path} -> {remote_path} (mode={mode})")
681
750
  local_dir = Path(local_path)
682
751
 
683
752
  def _check_path(ld=local_dir):
@@ -686,23 +755,27 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
686
755
  exists, is_dir = await asyncio.to_thread(_check_path)
687
756
  if not exists:
688
757
  SYLogger.error(f"[Sandbox] 本地目录不存在: {local_path}")
689
- results[local_path] = {"success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
758
+ results[local_path] = {
759
+ "success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
690
760
  continue
691
761
 
692
762
  if is_dir:
693
763
  SYLogger.info(f"[Sandbox] 开始上传目录: {local_path}")
694
764
  result = await self._upload_directory_async(str(local_dir), remote_path, mode=mode, timeout=timeout)
695
765
  results[local_path] = result
696
- SYLogger.info(f"[Sandbox] 目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
766
+ SYLogger.info(
767
+ f"[Sandbox] 目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
697
768
  else:
698
769
  # 单文件
699
770
  SYLogger.info(f"[Sandbox] 上传单文件: {local_path}")
700
771
  content = await asyncio.to_thread(lambda p: _read_file_safe(p), local_dir)
701
772
  upload_results = await self.aupload_files([(remote_path, content)], timeout=timeout)
702
773
  if upload_results[0].error:
703
- results[local_path] = {"success": 0, "failed": 1, "errors": [{"path": remote_path, "error": upload_results[0].error}]}
774
+ results[local_path] = {"success": 0, "failed": 1, "errors": [
775
+ {"path": remote_path, "error": upload_results[0].error}]}
704
776
  else:
705
- results[local_path] = {"success": 1, "failed": 0, "errors": []}
777
+ results[local_path] = {
778
+ "success": 1, "failed": 0, "errors": []}
706
779
 
707
780
  self._synced = True
708
781
  SYLogger.info("[Sandbox] _sync_async 完成")
@@ -728,7 +801,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
728
801
 
729
802
  await asyncio.to_thread(_check)
730
803
 
731
- SYLogger.info(f"[Sandbox] _upload_directory_async 开始: {local_dir} -> {remote_path} (mode={mode})")
804
+ SYLogger.info(
805
+ f"[Sandbox] _upload_directory_async 开始: {local_dir} -> {remote_path} (mode={mode})")
732
806
 
733
807
  # partial 模式:查询沙箱已有子目录并跳过
734
808
  existing_subdirs = set()
@@ -743,11 +817,13 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
743
817
  if item.get("is_dir", False):
744
818
  existing_subdirs.add(item.get("name", ""))
745
819
  if existing_subdirs:
746
- SYLogger.info(f"[Sandbox] partial 模式,跳过已有子目录: {existing_subdirs}")
820
+ SYLogger.info(
821
+ f"[Sandbox] partial 模式,跳过已有子目录: {existing_subdirs}")
747
822
  except Exception as e:
748
823
  SYLogger.warning(f"[Sandbox] 查询沙箱目录失败,将全量同步: {e}")
749
824
 
750
- exclude_patterns = [".git", "__pycache__", ".pyc", ".DS_Store", "node_modules"]
825
+ exclude_patterns = [".git", "__pycache__",
826
+ ".pyc", ".DS_Store", "node_modules"]
751
827
 
752
828
  def should_exclude(name: str) -> bool:
753
829
  for pattern in exclude_patterns:
@@ -768,7 +844,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
768
844
  try:
769
845
  rel = Path(root).relative_to(local_path)
770
846
  if str(rel) == ".":
771
- dirs[:] = [d for d in dirs if d not in existing_subdirs]
847
+ dirs[:] = [
848
+ d for d in dirs if d not in existing_subdirs]
772
849
  except ValueError:
773
850
  pass
774
851
 
@@ -810,15 +887,18 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
810
887
  upload_results = await self.aupload_files(batch_items, timeout=timeout)
811
888
  for r in upload_results:
812
889
  if r.error:
813
- all_results.append({"ok": False, "path": r.path, "error": r.error})
890
+ all_results.append(
891
+ {"ok": False, "path": r.path, "error": r.error})
814
892
  else:
815
893
  all_results.append({"ok": True})
816
894
 
817
895
  success_count = sum(1 for r in all_results if r["ok"])
818
896
  failed_count = sum(1 for r in all_results if not r["ok"])
819
- errors = [{"path": r["path"], "error": r["error"]} for r in all_results if not r["ok"]]
897
+ errors = [{"path": r["path"], "error": r["error"]}
898
+ for r in all_results if not r["ok"]]
820
899
 
821
- SYLogger.info(f"[Sandbox] _upload_directory_async 完成: 成功={success_count}, 失败={failed_count}")
900
+ SYLogger.info(
901
+ f"[Sandbox] _upload_directory_async 完成: 成功={success_count}, 失败={failed_count}")
822
902
  return {"success": success_count, "failed": failed_count, "errors": errors}
823
903
 
824
904
  # ============== 批量操作 ==============
@@ -855,7 +935,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
855
935
  "user_id": self._user_id
856
936
  }, timeout=timeout)
857
937
  return [
858
- FileUploadResponse(path=r.get("path", ""), error=r.get("error"))
938
+ FileUploadResponse(path=r.get("path", ""),
939
+ error=r.get("error"))
859
940
  for r in result.get("results", [])
860
941
  ]
861
942
  except Exception as e:
@@ -868,9 +949,11 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
868
949
  "content": base64.b64encode(content).decode(),
869
950
  "user_id": self._user_id
870
951
  }, timeout=timeout)
871
- results.append(FileUploadResponse(path=r["path"], error=r.get("error")))
952
+ results.append(FileUploadResponse(
953
+ path=r["path"], error=r.get("error")))
872
954
  except Exception as ex:
873
- results.append(FileUploadResponse(path=path, error=str(ex)))
955
+ results.append(FileUploadResponse(
956
+ path=path, error=str(ex)))
874
957
  return results
875
958
 
876
959
  def download_files(self, paths: List[str]) -> List[FileDownloadResponse]:
@@ -889,7 +972,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
889
972
  ))
890
973
  else:
891
974
  content = base64.b64decode(result["content"])
892
- results.append(FileDownloadResponse(path=path, content=content))
975
+ results.append(FileDownloadResponse(
976
+ path=path, content=content))
893
977
  except Exception as e:
894
978
  results.append(FileDownloadResponse(path=path, error=str(e)))
895
979
  return results
@@ -911,7 +995,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
911
995
  ))
912
996
  else:
913
997
  content = base64.b64decode(result["content"])
914
- results.append(FileDownloadResponse(path=path, content=content))
998
+ results.append(FileDownloadResponse(
999
+ path=path, content=content))
915
1000
  except Exception as e:
916
1001
  results.append(FileDownloadResponse(path=path, error=str(e)))
917
1002
  return results
@@ -929,19 +1014,23 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
929
1014
  for idx, entry in enumerate(self._sync_dirs):
930
1015
  local_path, remote_path = entry[0], entry[1]
931
1016
  mode = entry[2] if len(entry) > 2 else "full"
932
- SYLogger.info(f"[Sandbox] 同步目录 [{idx+1}/{len(self._sync_dirs)}]: {local_path} -> {remote_path} (mode={mode})")
1017
+ SYLogger.info(
1018
+ f"[Sandbox] 同步目录 [{idx+1}/{len(self._sync_dirs)}]: {local_path} -> {remote_path} (mode={mode})")
933
1019
  local_dir = Path(local_path)
934
1020
 
935
1021
  if not local_dir.exists():
936
1022
  SYLogger.error(f"[Sandbox] 本地目录不存在: {local_path}")
937
- results[local_path] = {"success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
1023
+ results[local_path] = {
1024
+ "success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
938
1025
  continue
939
1026
 
940
1027
  if local_dir.is_dir():
941
1028
  SYLogger.info(f"[Sandbox] 开始上传目录: {local_path}")
942
- result = self._upload_directory_sync(str(local_dir), remote_path, mode=mode)
1029
+ result = self._upload_directory_sync(
1030
+ str(local_dir), remote_path, mode=mode)
943
1031
  results[local_path] = result
944
- SYLogger.info(f"[Sandbox] 目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
1032
+ SYLogger.info(
1033
+ f"[Sandbox] 目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
945
1034
  else:
946
1035
  # 单文件
947
1036
  SYLogger.info(f"[Sandbox] 上传单文件: {local_path}")
@@ -949,9 +1038,11 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
949
1038
  content = f.read()
950
1039
  upload_results = self.upload_files([(remote_path, content)])
951
1040
  if upload_results[0].error:
952
- results[local_path] = {"success": 0, "failed": 1, "errors": [{"path": remote_path, "error": upload_results[0].error}]}
1041
+ results[local_path] = {"success": 0, "failed": 1, "errors": [
1042
+ {"path": remote_path, "error": upload_results[0].error}]}
953
1043
  else:
954
- results[local_path] = {"success": 1, "failed": 0, "errors": []}
1044
+ results[local_path] = {
1045
+ "success": 1, "failed": 0, "errors": []}
955
1046
 
956
1047
  self._synced = True
957
1048
  SYLogger.info("[Sandbox] _sync_sync 完成")
@@ -985,19 +1076,23 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
985
1076
  SYLogger.info(f"[Sandbox] sync_dirs 开始, 共 {len(dirs)} 个目录")
986
1077
  results = {}
987
1078
  for idx, (local_path, remote_path) in enumerate(dirs):
988
- SYLogger.info(f"[Sandbox] 同步目录 [{idx+1}/{len(dirs)}]: {local_path} -> {remote_path}")
1079
+ SYLogger.info(
1080
+ f"[Sandbox] 同步目录 [{idx+1}/{len(dirs)}]: {local_path} -> {remote_path}")
989
1081
  local_dir = Path(local_path)
990
1082
 
991
1083
  if not local_dir.exists():
992
1084
  SYLogger.warning(f"[Sandbox] 本地目录不存在: {local_path}")
993
- results[local_path] = {"success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
1085
+ results[local_path] = {
1086
+ "success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
994
1087
  continue
995
1088
 
996
1089
  if local_dir.is_dir():
997
1090
  SYLogger.info(f"[Sandbox] 开始上传目录: {local_path}")
998
- result = self._upload_directory_sync(str(local_dir), remote_path)
1091
+ result = self._upload_directory_sync(
1092
+ str(local_dir), remote_path)
999
1093
  results[local_path] = result
1000
- SYLogger.info(f"[Sandbox] 目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
1094
+ SYLogger.info(
1095
+ f"[Sandbox] 目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
1001
1096
  else:
1002
1097
  # 单文件
1003
1098
  SYLogger.info(f"[Sandbox] 上传单文件: {local_path}")
@@ -1005,9 +1100,11 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1005
1100
  content = f.read()
1006
1101
  upload_results = self.upload_files([(remote_path, content)])
1007
1102
  if upload_results[0].error:
1008
- results[local_path] = {"success": 0, "failed": 1, "errors": [{"path": remote_path, "error": upload_results[0].error}]}
1103
+ results[local_path] = {"success": 0, "failed": 1, "errors": [
1104
+ {"path": remote_path, "error": upload_results[0].error}]}
1009
1105
  else:
1010
- results[local_path] = {"success": 1, "failed": 0, "errors": []}
1106
+ results[local_path] = {
1107
+ "success": 1, "failed": 0, "errors": []}
1011
1108
 
1012
1109
  SYLogger.info("[Sandbox] sync_dirs 完成")
1013
1110
  return results
@@ -1028,7 +1125,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1028
1125
  SYLogger.info(f"[Sandbox] async_sync_dirs 开始, 共 {len(dirs)} 个目录")
1029
1126
  results = {}
1030
1127
  for idx, (local_path, remote_path) in enumerate(dirs):
1031
- SYLogger.info(f"[Sandbox] 异步同步目录 [{idx+1}/{len(dirs)}]: {local_path} -> {remote_path}")
1128
+ SYLogger.info(
1129
+ f"[Sandbox] 异步同步目录 [{idx+1}/{len(dirs)}]: {local_path} -> {remote_path}")
1032
1130
  local_dir = Path(local_path)
1033
1131
 
1034
1132
  def _check_path(ld=local_dir):
@@ -1037,22 +1135,26 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1037
1135
  exists, is_dir = await asyncio.to_thread(_check_path)
1038
1136
  if not exists:
1039
1137
  SYLogger.warning(f"[Sandbox] 本地目录不存在: {local_path}")
1040
- results[local_path] = {"success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
1138
+ results[local_path] = {
1139
+ "success": 0, "failed": 0, "errors": [{"error": "目录不存在"}]}
1041
1140
  continue
1042
1141
 
1043
1142
  if is_dir:
1044
1143
  SYLogger.info(f"[Sandbox] 开始异步上传目录: {local_path}")
1045
1144
  result = await self._upload_directory_async(str(local_dir), remote_path, timeout=timeout)
1046
1145
  results[local_path] = result
1047
- SYLogger.info(f"[Sandbox] 异步目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
1146
+ SYLogger.info(
1147
+ f"[Sandbox] 异步目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
1048
1148
  else:
1049
1149
  SYLogger.info(f"[Sandbox] 异步上传单文件: {local_path}")
1050
1150
  content = await asyncio.to_thread(lambda p: _read_file_safe(p), local_dir)
1051
1151
  upload_results = await self.aupload_files([(remote_path, content)], timeout=timeout)
1052
1152
  if upload_results[0].error:
1053
- results[local_path] = {"success": 0, "failed": 1, "errors": [{"path": remote_path, "error": upload_results[0].error}]}
1153
+ results[local_path] = {"success": 0, "failed": 1, "errors": [
1154
+ {"path": remote_path, "error": upload_results[0].error}]}
1054
1155
  else:
1055
- results[local_path] = {"success": 1, "failed": 0, "errors": []}
1156
+ results[local_path] = {
1157
+ "success": 1, "failed": 0, "errors": []}
1056
1158
 
1057
1159
  SYLogger.info("[Sandbox] async_sync_dirs 完成")
1058
1160
  return results
@@ -1077,7 +1179,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1077
1179
  if not local_path.is_dir():
1078
1180
  raise NotADirectoryError(f"路径不是目录: {local_dir}")
1079
1181
 
1080
- SYLogger.info(f"[Sandbox] _upload_directory_sync 开始: {local_dir} -> {remote_path} (mode={mode})")
1182
+ SYLogger.info(
1183
+ f"[Sandbox] _upload_directory_sync 开始: {local_dir} -> {remote_path} (mode={mode})")
1081
1184
 
1082
1185
  # partial 模式:查询沙箱已有子目录并跳过
1083
1186
  existing_subdirs = set()
@@ -1092,11 +1195,13 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1092
1195
  if item.get("is_dir", False):
1093
1196
  existing_subdirs.add(item.get("name", ""))
1094
1197
  if existing_subdirs:
1095
- SYLogger.info(f"[Sandbox] partial 模式,跳过已有子目录: {existing_subdirs}")
1198
+ SYLogger.info(
1199
+ f"[Sandbox] partial 模式,跳过已有子目录: {existing_subdirs}")
1096
1200
  except Exception as e:
1097
1201
  SYLogger.warning(f"[Sandbox] 查询沙箱目录失败,将全量同步: {e}")
1098
1202
 
1099
- exclude_patterns = [".git", "__pycache__", ".pyc", ".DS_Store", "node_modules"]
1203
+ exclude_patterns = [".git", "__pycache__",
1204
+ ".pyc", ".DS_Store", "node_modules"]
1100
1205
 
1101
1206
  def should_exclude(name: str) -> bool:
1102
1207
  for pattern in exclude_patterns:
@@ -1117,7 +1222,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1117
1222
  try:
1118
1223
  rel = Path(root).relative_to(local_path)
1119
1224
  if str(rel) == ".":
1120
- skipped_names = [d for d in dirs if d in existing_subdirs]
1225
+ skipped_names = [
1226
+ d for d in dirs if d in existing_subdirs]
1121
1227
  dirs[:] = [d for d in dirs if d not in existing_subdirs]
1122
1228
  if skipped_names:
1123
1229
  SYLogger.info(f"[Sandbox] 跳过已有目录: {skipped_names}")
@@ -1157,7 +1263,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1157
1263
  if result.get("error"):
1158
1264
  with lock:
1159
1265
  failed_count += 1
1160
- errors.append({"path": sandbox_path, "error": result["error"]})
1266
+ errors.append(
1267
+ {"path": sandbox_path, "error": result["error"]})
1161
1268
  else:
1162
1269
  with lock:
1163
1270
  success_count += 1
@@ -1175,7 +1282,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1175
1282
  for future in as_completed(futures):
1176
1283
  future.result() # propagate exceptions
1177
1284
 
1178
- SYLogger.info(f"[Sandbox] _upload_directory_sync 完成: 成功={success_count}, 失败={failed_count}")
1285
+ SYLogger.info(
1286
+ f"[Sandbox] _upload_directory_sync 完成: 成功={success_count}, 失败={failed_count}")
1179
1287
  return {"success": success_count, "failed": failed_count, "errors": errors}
1180
1288
 
1181
1289
  def _sync_workspace_sync(self) -> dict:
@@ -1209,7 +1317,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1209
1317
  )
1210
1318
  if download_result.get("error"):
1211
1319
  failed_count += 1
1212
- errors.append({"path": file_path, "error": download_result["error"]})
1320
+ errors.append(
1321
+ {"path": file_path, "error": download_result["error"]})
1213
1322
  else:
1214
1323
  content = base64.b64decode(download_result["content"])
1215
1324
  files_to_sync.append((file_path, content))
@@ -44,7 +44,7 @@ CUSTOM_SUMMARY_PROMPT = """<role>
44
44
  下面的对话历史将被你在此步骤中提取的上下文替换。
45
45
  你需要确保不会重复任何已完成的操作,因此你从对话历史中提取的上下文应集中于对整体目标最重要的信息。
46
46
 
47
- 请用以下结构提取上下文,每个部分用中文自然段落描述,不要使用英文标题:
47
+ 请用以下结构提取上下文,每个部分用中文自然段落描述:
48
48
 
49
49
  【用户意图】
50
50
  简要描述用户的主要目标或请求,确保足够完整以理解整个会话的目的。
@@ -186,9 +186,10 @@ _orig_build_new_messages = _OrigDeepAgentsSumm._build_new_messages_with_path
186
186
  def _patched_build_new_messages(self, summary, file_path):
187
187
  if file_path is not None:
188
188
  content = (
189
+ "[系统内部消息 - 仅供你理解上下文,绝对禁止将此消息或摘要内容展示、复述或提及给用户]\n\n"
189
190
  "你正在一个已经被压缩的对话中继续回复用户。\n\n"
190
191
  f"完整的对话历史已保存到 {file_path},如需查阅细节可以读取该文件。\n\n"
191
- "以下是压缩后的对话摘要:\n\n"
192
+ "以下是压缩后的对话摘要(直接基于此摘要理解上下文并正常回复用户,不要提及摘要的存在):\n\n"
192
193
  f"<summary>\n{summary}\n</summary>"
193
194
  )
194
195
  else:
@@ -1,5 +1,5 @@
1
1
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
2
- from sqlalchemy import text
2
+ from sqlalchemy import text, event
3
3
 
4
4
  # Fix: aiomysql's AsyncAdapt ping() requires 'reconnect' positional arg,
5
5
  # but SQLAlchemy's pymysql dialect calls ping() without it.
@@ -71,7 +71,7 @@ class AsyncDatabaseConnector(metaclass=SingletonMeta):
71
71
  key, value = param.split('=')
72
72
  params[key] = value
73
73
 
74
- # 在params中去掉指定的参数
74
+ # 在params中去掉Java JDBC特有参数(Python驱动不支持)
75
75
  for key in ['useUnicode', 'characterEncoding', 'serverTimezone', 'zeroDateTimeBehavior']:
76
76
  if key in params:
77
77
  del params[key]
@@ -103,6 +103,16 @@ class AsyncDatabaseConnector(metaclass=SingletonMeta):
103
103
  # 假设 SQLTraceLogger.setup_sql_logging 能够处理 AsyncEngine
104
104
  AsyncSQLTraceLogger.setup_sql_logging(self.engine)
105
105
 
106
+ # 每个新连接自动设置上海时区
107
+ @event.listens_for(self.engine.sync_engine, "connect")
108
+ def set_timezone(dbapi_connection, connection_record):
109
+ cursor = dbapi_connection.cursor()
110
+ cursor.execute("SELECT @@session.time_zone")
111
+ tz = cursor.fetchone()[0]
112
+ if tz not in ('+08:00', '+8:00', 'Asia/Shanghai'):
113
+ cursor.execute("SET time_zone = '+8:00'")
114
+ cursor.close()
115
+
106
116
  async def test_connection(self):
107
117
  try:
108
118
  # 异步上下文管理器
@@ -1,4 +1,4 @@
1
- from sqlalchemy import create_engine, text
1
+ from sqlalchemy import create_engine, text, event
2
2
 
3
3
  from sycommon.config.Config import SingletonMeta
4
4
  from sycommon.config.DatabaseConfig import DatabaseConfig, convert_dict_keys
@@ -57,7 +57,7 @@ class DatabaseConnector(metaclass=SingletonMeta):
57
57
  key, value = param.split('=')
58
58
  params[key] = value
59
59
 
60
- # 在params中去掉指定的参数
60
+ # 在params中去掉Java JDBC特有参数(Python驱动不支持)
61
61
  for key in ['useUnicode', 'characterEncoding', 'serverTimezone', 'zeroDateTimeBehavior']:
62
62
  if key in params:
63
63
  del params[key]
@@ -84,6 +84,16 @@ class DatabaseConnector(metaclass=SingletonMeta):
84
84
  # 注册 SQL 日志拦截器
85
85
  SQLTraceLogger.setup_sql_logging(self.engine)
86
86
 
87
+ # 每个新连接自动设置上海时区
88
+ @event.listens_for(self.engine, "connect")
89
+ def set_timezone(dbapi_connection, connection_record):
90
+ cursor = dbapi_connection.cursor()
91
+ cursor.execute("SELECT @@session.time_zone")
92
+ tz = cursor.fetchone()[0]
93
+ if tz not in ('+08:00', '+8:00', 'Asia/Shanghai'):
94
+ cursor.execute("SET time_zone = '+8:00'")
95
+ cursor.close()
96
+
87
97
  # 测试
88
98
  if not self.test_connection():
89
99
  raise Exception("Database connection test failed")
@@ -166,15 +166,19 @@ class KafkaSink:
166
166
 
167
167
  def close(self):
168
168
  """关闭 KafkaProducer,释放连接池资源"""
169
- if self._producer:
170
- self._producer = None
171
- try:
172
- self._producer_obj.flush(timeout=5)
173
- self._producer_obj.close(timeout=10)
174
- except KeyboardInterrupt:
175
- pass
176
- except Exception:
177
- pass
169
+ if self._producer_obj is None:
170
+ return
171
+ # 先移除引用,阻止 write() 继续发送新消息
172
+ self._producer = None
173
+ try:
174
+ self._producer_obj.flush(timeout=5)
175
+ self._producer_obj.close(timeout=10)
176
+ except KeyboardInterrupt:
177
+ pass
178
+ except Exception:
179
+ pass
180
+ finally:
181
+ self._producer_obj = None
178
182
 
179
183
 
180
184
  class _InterceptHandler(logging.Handler):
@@ -105,23 +105,34 @@ def _create_head_tail_preview(
105
105
  def _convert_list_content_to_str(content: list, tool_name: str) -> str:
106
106
  """将 list 类型的 ToolMessage.content 转换为字符串。
107
107
 
108
- 对于图片类型(type=image 且含 base64),转换为提示模型自行用 execute 处理的说明。
108
+ 对于非 text 类型的 content block(image/file/audio/video 等),
109
+ 转换为文本描述,防止上游 API 返回 type 类型错误。
109
110
  对于文本类型,保留完整文本。
110
111
  """
111
112
  parts = []
112
113
  for item in content:
113
114
  if isinstance(item, dict):
114
115
  item_type = item.get("type", "text")
115
- if item_type == "image" and "base64" in item:
116
+ if item_type == "text" and "text" in item:
117
+ parts.append(item["text"])
118
+ elif item_type == "image" and "base64" in item:
116
119
  b64 = item["base64"]
117
120
  mime = item.get("mime_type", "image/unknown")
118
- size_kb = len(b64) * 3 // 4 // 1024 # base64 -> bytes -> KB
121
+ size_kb = len(b64) * 3 // 4 // 1024
119
122
  parts.append(
120
123
  f"[图片文件 ({mime}, {size_kb}KB) — "
121
- f"图片二进制数据无法直接发送给模型。"
124
+ f"图片二进制内容已省略。"
122
125
  f"请使用 execute 工具通过 Python (如 PIL/OpenCV) 读取并处理图片,"
123
126
  f"例如 OCR 识别文字、分析图片内容等。]"
124
127
  )
128
+ elif "base64" in item:
129
+ mime = item.get("mime_type", "application/octet-stream")
130
+ size_kb = len(item["base64"]) * 3 // 4 // 1024
131
+ parts.append(
132
+ f"[{item_type}文件 ({mime}, {size_kb}KB) — "
133
+ f"二进制文件内容已省略。"
134
+ f"请使用 execute 工具通过 Python 处理该文件。]"
135
+ )
125
136
  elif "text" in item:
126
137
  parts.append(item["text"])
127
138
  else:
@@ -219,17 +230,18 @@ class ToolResultTruncationMiddleware(AgentMiddleware):
219
230
 
220
231
  content = result.content
221
232
 
222
- # list 类型 content:如 read_file 读取图片返回的 base64 数据
223
- # 转换为字符串描述,防止上游 API 拒绝 list content
233
+ # list 类型 content:如 read_file 读取非文本文件返回的 content_blocks
234
+ # 只要含有非 text 类型的 block,就转换为字符串描述
235
+ # 防止上游 API 拒绝 type=file/image/audio 等 content block
224
236
  if isinstance(content, list):
225
- has_image = any(
226
- isinstance(item, dict) and item.get("type") == "image" and "base64" in item
237
+ has_non_text = any(
238
+ isinstance(item, dict) and item.get("type") != "text"
227
239
  for item in content
228
240
  )
229
- if has_image:
241
+ if has_non_text:
230
242
  new_content = _convert_list_content_to_str(content, tool_name)
231
243
  SYLogger.info(
232
- f"[ToolResultTruncation] tool='{tool_name}' converted image "
244
+ f"[ToolResultTruncation] tool='{tool_name}' converted "
233
245
  f"list content to string ({len(new_content)} chars)")
234
246
  return ToolMessage(
235
247
  content=new_content,
@@ -250,25 +250,20 @@ def setup_trace_id_handler(app):
250
250
 
251
251
  # 构建响应日志
252
252
  response_log_body = response_body.decode('utf-8', errors='ignore')
253
- total_len = len(response_log_body)
254
- # 检测是否包含 base64 二进制内容(如 PDF、图片等)
255
- is_binary_response = False
253
+ # 检测是否包含 base64 二进制内容(如 PDF、图片等),如有则省略
256
254
  try:
257
255
  resp_json = json.loads(response_log_body)
258
256
  if isinstance(resp_json, dict) and resp_json.get("content") and len(str(resp_json["content"])) > 1000:
259
257
  resp_json["content"] = f"... (base64/binary content omitted, {len(str(resp_json['content']))} chars)"
260
258
  response_log_body = json.dumps(resp_json, ensure_ascii=False)
261
- is_binary_response = True
262
259
  except (json.JSONDecodeError, ValueError):
263
260
  pass
264
- if not is_binary_response and total_len > 10000:
265
- response_log_body = response_log_body[:10000] + f"... (truncated, total {total_len} bytes)"
266
261
  response_message = {
267
262
  "traceId": trace_id,
268
263
  "status_code": response.status_code,
269
264
  "response_body": response_log_body,
270
265
  }
271
- SYLogger.info(json.dumps(response_message, ensure_ascii=False))
266
+ SYLogger.info(response_message)
272
267
 
273
268
  # 兜底:确保 Header 必有 TraceId
274
269
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.5a29
3
+ Version: 0.2.5a31
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -131,13 +131,13 @@ sycommon/agent/agent_manager.py,sha256=UhhaekEumT7g4v_Z1UB4jTp13X0n8M8erYaQdkGGW
131
131
  sycommon/agent/chat_events.py,sha256=t7qWa6OrIWLfqtd1AnqaVP67QWkn1JxpCBTZYQpoLuM,14226
132
132
  sycommon/agent/deep_agent.py,sha256=zZRmjEYAEx8LgnDCRFf6QS9Xxyy67WSzomjdC2I732Q,55405
133
133
  sycommon/agent/multi_agent_team.py,sha256=W5gP6sWIYhhXf6iBLHfyTIs9eAhUdkWrn_PF6W2cESs,26059
134
- sycommon/agent/summarization_utils.py,sha256=q45O-a3_H21Pm1vSIskSK0VCoheilGX5ONnC9Q7PUBE,10682
134
+ sycommon/agent/summarization_utils.py,sha256=5eiLtxRZKTzO0tnr8sVaL-H3a0eUtvXpCa28Jw_j2eE,10889
135
135
  sycommon/agent/mcp/__init__.py,sha256=iKrdDhIrFsNIkqG_kgcwNe-nOiM6uVfolKv44LfQ-FQ,636
136
136
  sycommon/agent/mcp/models.py,sha256=zda4ho-UTYOJ5oPAZPZ-vxhfDRLghUCsMroHnA2A2QQ,1490
137
137
  sycommon/agent/mcp/tool_loader.py,sha256=iJkKSBClm348xeQ0CmK4lOUbLEQ-CRr-GvJdPmSHg9o,9916
138
138
  sycommon/agent/sandbox/__init__.py,sha256=jR7LlkD4J4Y6QYyRXQClkwmqDBCCPmycV_hQV9p9YHw,4621
139
139
  sycommon/agent/sandbox/file_ops.py,sha256=C1_gfarr2aHstaB9VzV_APJYgQtRfUVvkc3b7QIgmc0,24690
140
- sycommon/agent/sandbox/http_sandbox_backend.py,sha256=Mn50y5b-hfrQPX7Q6bvZfqv_PnKdad49PyE4B7DDQ7g,56297
140
+ sycommon/agent/sandbox/http_sandbox_backend.py,sha256=_l7guM-sv4XduE9NTNVLPyRTjZZIGR4qefJUTc8e5q4,59199
141
141
  sycommon/agent/sandbox/minio_sync.py,sha256=d1kuWllvyAvAMsFZCP0OdHEQtXN9BEIgHbupC31BjSk,20000
142
142
  sycommon/agent/sandbox/sandbox_pool.py,sha256=eMn8sLakCWf90l6ni2-333QM8oBdX1CflV-WzneFp_k,9133
143
143
  sycommon/agent/sandbox/sandbox_recovery.py,sha256=X-eDODx1tmGMh_iTngV6e1ppfDBHpTdkPreJusN5MHY,7358
@@ -162,9 +162,9 @@ sycommon/config/SentryConfig.py,sha256=OsLb3G9lTsCSZ7tWkcXWJHmvfILQopBxje5pjnkFJ
162
162
  sycommon/config/XxlJobConfig.py,sha256=VSG6dn9ysfUVunOs7PqugyZUGJWmX_cEePz2ZCfqHtU,392
163
163
  sycommon/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
164
  sycommon/database/async_base_db_service.py,sha256=yo3hhz4UlKtYBY3DoyJjqXfDJPNfhNdGivvzPkW2S_I,4469
165
- sycommon/database/async_database_service.py,sha256=Cf3RaO3skP6IAJrkta-CRE-Q1NtjWLPLUe9hazB8LRM,4873
165
+ sycommon/database/async_database_service.py,sha256=UPQ4Y0Cpioyav5tdFHr40lvkV5uTH2UZRS853663fy4,5380
166
166
  sycommon/database/base_db_service.py,sha256=urdAibaCUt3hxwwKzEMBceWkIwsw3ja_WDJ1QKc8nLU,4116
167
- sycommon/database/database_service.py,sha256=IMoJ9554dYkr6QfRofvNa0VR24U1WQDz_ATrg0-6EQ0,3857
167
+ sycommon/database/database_service.py,sha256=rbbII50EI4VTTkwrFiw9PnvW6LgR5yQvN8aJblUu1Oc,4352
168
168
  sycommon/database/elasticsearch_service.py,sha256=qm490GRlxZlYsQgyfyclSbARRP1-Tc4Lwav3lbPINvQ,3092
169
169
  sycommon/database/pg_checkpoint_service.py,sha256=wYVVgIPCsXD0fdwADTJpaRfjqVN0twptJva_ROWZEDQ,5203
170
170
  sycommon/database/redis_service.py,sha256=tPw8UgeuyYQBxWfPRjx7VqlSRFNxIsnR0WSGd36GaA8,20509
@@ -195,7 +195,7 @@ sycommon/llm/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4,sha256=Ijkh
195
195
  sycommon/llm/tiktoken_cache/fb374d419588a4632f3f557e76b4b70aebbca790,sha256=RGqVOMtsNI41FhINfAiwn1fDZJXirP_-WaW_iwz7Gi0,3613922
196
196
  sycommon/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
197
197
  sycommon/logging/async_sql_logger.py,sha256=_OY36XkUm__U3NhMgiecy-qd-nptZ_0gpE3J8lGAr58,2619
198
- sycommon/logging/kafka_log.py,sha256=nhM73AbwdAFVpP-_8mCG74ZXCOzgSfPEAfIK2y0wRpw,13191
198
+ sycommon/logging/kafka_log.py,sha256=5tbCYMLEm7RwfWoYQMZU96xjaEGG6T0QfBhglxT5Q8E,13310
199
199
  sycommon/logging/logger_levels.py,sha256=_-uQ_T1N8NkNgcAmLrMmJ83nHTDw5ZNvXFPvdk89XGY,1144
200
200
  sycommon/logging/logger_wrapper.py,sha256=TtzmGw_K5NjBGBq9Gjqtnh834izAv8TQwsOeL2fIbAI,481
201
201
  sycommon/logging/process_logger.py,sha256=rjunPTaDejtusvobSrc3G44DEqnt_bbNSY3r8cWqBsY,5895
@@ -213,8 +213,8 @@ sycommon/middleware/mq.py,sha256=9X6KKtadFjBXKS5L3kEKujYio9wwGfWgXwWOAHO-HDg,254
213
213
  sycommon/middleware/sandbox.py,sha256=er0zeHLQtHJqSr27fGcKbAtRo1ZOQ-Lqif8ALWNFSak,47858
214
214
  sycommon/middleware/timeout.py,sha256=KlxOPa8xl2dg6yuRi_EzkVJG8bX4stb5ueYxctzzGM8,1433
215
215
  sycommon/middleware/token_tracking.py,sha256=rEbgV1bgWMdzAERx4aq5XAvOIT6jTY_tK1P0xHJnL3o,6609
216
- sycommon/middleware/tool_result_truncation.py,sha256=C5739a3HkpiEPLplTnBTGcVOUvkd-lQBl3oYsM0_7rI,10129
217
- sycommon/middleware/traceid.py,sha256=HX4zg7Tp_mPNr2eWDDvK3f7GFJ6eOSeW062Xcc-xshc,14340
216
+ sycommon/middleware/tool_result_truncation.py,sha256=307qUekFbhrh5x8rLYgoOOSH1Ph7doTicrpTiyiZo_A,10715
217
+ sycommon/middleware/traceid.py,sha256=MrcHj-2hzwl5sOMbf6-34tezYv4COnh3R8vaCN9zRuA,14025
218
218
  sycommon/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
219
  sycommon/models/base_http.py,sha256=EICAAibx3xhjBsLqm35Mi3DCqxp0FME4rD_3iQVjT_E,3051
220
220
  sycommon/models/log.py,sha256=rZpj6VkDRxK3B6H7XSeWdYZshU8F0Sks8bq1p6pPlDw,500
@@ -279,8 +279,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
279
279
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
280
280
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
281
281
  sycommon/xxljob/xxljob_service.py,sha256=JIEJaGXhqrTLcyxlyynSrsHg9bBnDNzX-D4qIWLRPUE,6815
282
- sycommon_python_lib-0.2.5a29.dist-info/METADATA,sha256=Fm8XMMlZubGy8kCj8-tsd-eTL0DE6XxA-y7TFS1gpM0,7914
283
- sycommon_python_lib-0.2.5a29.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
284
- sycommon_python_lib-0.2.5a29.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
285
- sycommon_python_lib-0.2.5a29.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
286
- sycommon_python_lib-0.2.5a29.dist-info/RECORD,,
282
+ sycommon_python_lib-0.2.5a31.dist-info/METADATA,sha256=QP-iiLOW9iiICa42zGhSK_x_iWW2TTmRNp-9IMhee6c,7914
283
+ sycommon_python_lib-0.2.5a31.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
284
+ sycommon_python_lib-0.2.5a31.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
285
+ sycommon_python_lib-0.2.5a31.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
286
+ sycommon_python_lib-0.2.5a31.dist-info/RECORD,,