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.
- sycommon/agent/sandbox/http_sandbox_backend.py +178 -69
- sycommon/agent/summarization_utils.py +3 -2
- sycommon/database/async_database_service.py +12 -2
- sycommon/database/database_service.py +12 -2
- sycommon/logging/kafka_log.py +13 -9
- sycommon/middleware/tool_result_truncation.py +22 -10
- sycommon/middleware/traceid.py +2 -7
- {sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/RECORD +12 -12
- {sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/top_level.txt +0 -0
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
395
|
-
|
|
396
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
574
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
607
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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] = {
|
|
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(
|
|
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": [
|
|
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] = {
|
|
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(
|
|
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(
|
|
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__",
|
|
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[:] = [
|
|
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(
|
|
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"]}
|
|
897
|
+
errors = [{"path": r["path"], "error": r["error"]}
|
|
898
|
+
for r in all_results if not r["ok"]]
|
|
820
899
|
|
|
821
|
-
SYLogger.info(
|
|
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", ""),
|
|
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(
|
|
952
|
+
results.append(FileUploadResponse(
|
|
953
|
+
path=r["path"], error=r.get("error")))
|
|
872
954
|
except Exception as ex:
|
|
873
|
-
results.append(FileUploadResponse(
|
|
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(
|
|
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(
|
|
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(
|
|
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] = {
|
|
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(
|
|
1029
|
+
result = self._upload_directory_sync(
|
|
1030
|
+
str(local_dir), remote_path, mode=mode)
|
|
943
1031
|
results[local_path] = result
|
|
944
|
-
SYLogger.info(
|
|
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": [
|
|
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] = {
|
|
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(
|
|
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] = {
|
|
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(
|
|
1091
|
+
result = self._upload_directory_sync(
|
|
1092
|
+
str(local_dir), remote_path)
|
|
999
1093
|
results[local_path] = result
|
|
1000
|
-
SYLogger.info(
|
|
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": [
|
|
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] = {
|
|
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(
|
|
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] = {
|
|
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(
|
|
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": [
|
|
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] = {
|
|
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(
|
|
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(
|
|
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__",
|
|
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 = [
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
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")
|
sycommon/logging/kafka_log.py
CHANGED
|
@@ -166,15 +166,19 @@ class KafkaSink:
|
|
|
166
166
|
|
|
167
167
|
def close(self):
|
|
168
168
|
"""关闭 KafkaProducer,释放连接池资源"""
|
|
169
|
-
if self.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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 == "
|
|
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
|
|
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
|
|
223
|
-
#
|
|
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
|
-
|
|
226
|
-
isinstance(item, dict) and item.get("type")
|
|
237
|
+
has_non_text = any(
|
|
238
|
+
isinstance(item, dict) and item.get("type") != "text"
|
|
227
239
|
for item in content
|
|
228
240
|
)
|
|
229
|
-
if
|
|
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
|
|
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,
|
sycommon/middleware/traceid.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
266
|
+
SYLogger.info(response_message)
|
|
272
267
|
|
|
273
268
|
# 兜底:确保 Header 必有 TraceId
|
|
274
269
|
try:
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
217
|
-
sycommon/middleware/traceid.py,sha256=
|
|
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.
|
|
283
|
-
sycommon_python_lib-0.2.
|
|
284
|
-
sycommon_python_lib-0.2.
|
|
285
|
-
sycommon_python_lib-0.2.
|
|
286
|
-
sycommon_python_lib-0.2.
|
|
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,,
|
|
File without changes
|
{sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.5a29.dist-info → sycommon_python_lib-0.2.5a31.dist-info}/top_level.txt
RENAMED
|
File without changes
|