sycommon-python-lib 0.2.7a29__py3-none-any.whl → 0.2.7a31__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.
@@ -35,6 +35,7 @@
35
35
  """
36
36
 
37
37
  import asyncio
38
+ import hashlib
38
39
  import json
39
40
  import os
40
41
  import re
@@ -1366,6 +1367,79 @@ def _parse_skill_version(content: str) -> str:
1366
1367
  return ""
1367
1368
 
1368
1369
 
1370
+ def _decode_remote_skillmd(content: str) -> str:
1371
+ """把沙箱 /read 返回的 SKILL.md 内容统一解成明文。
1372
+
1373
+ 沙箱 /read 对大文件(如 skill-creator 的 34KB SKILL.md)返回 base64,
1374
+ 且 encoding 标志位可能漏标/误标。判断策略(不依赖 encoding 字段):
1375
+ - 明文 SKILL.md 一定含 frontmatter 标志 `---`;含则当明文直接返回。
1376
+ - 不含 `---` 则当作 base64,解码后返回;解码失败或解码后仍无 `---` 返回原值。
1377
+ """
1378
+ if not content:
1379
+ return content
1380
+ if "---" in content:
1381
+ return content # 明文
1382
+ try:
1383
+ import base64 as _b64
1384
+ decoded = _b64.b64decode(content).decode("utf-8", errors="replace")
1385
+ return decoded
1386
+ except Exception:
1387
+ return content
1388
+
1389
+
1390
+ def _skill_signature(content: str) -> str:
1391
+ """计算 SKILL.md 正文内容的指纹,用于检测「版本号一致但正文未同步」的脏数据。
1392
+
1393
+ 取正文(去掉 frontmatter)的前 N 行规范化后的拼接 + md5,作为轻量指纹。
1394
+ 版本号相同时若指纹不同,说明沙箱里的正文是旧的,需强制重传。
1395
+ frontmatter 跳过逻辑与 _parse_skill_version 一致(逐行数 ---)。
1396
+ """
1397
+ lines = content.split("\n")
1398
+ in_frontmatter = False
1399
+ body_start = 0
1400
+ for i, line in enumerate(lines):
1401
+ if line.strip() == "---":
1402
+ if not in_frontmatter:
1403
+ in_frontmatter = True # 进入 frontmatter
1404
+ else:
1405
+ body_start = i + 1 # 离开 frontmatter,正文从这里开始
1406
+ break
1407
+ body = "\n".join(lines[body_start:])
1408
+ # 规范化:去掉空白行和首尾空格,只取前 40 行做指纹(足够区分新旧大改)
1409
+ norm_lines = [l.rstrip() for l in body.split("\n") if l.strip()]
1410
+ head = "\n".join(norm_lines[:40])
1411
+ return f"{len(norm_lines)}:{hashlib.md5(head.encode('utf-8')).hexdigest()[:12]}"
1412
+
1413
+
1414
+ async def _verify_skill_content_fresh(
1415
+ backend, sandbox_dest: str, skill_name: str, local_src: str
1416
+ ) -> bool:
1417
+ """版本一致时,再校验沙箱 SKILL.md 正文指纹是否与本地一致。
1418
+ 一致返回 True(可跳过同步);不一致返回 False(需强制重传)。
1419
+ 任何读取异常返回 False(保守起见触发重传,宁可多传不可漏传)。"""
1420
+ local_md = os.path.join(local_src, "SKILL.md")
1421
+ if not os.path.isfile(local_md):
1422
+ return True # 本地没 SKILL.md,无法校验,交给版本逻辑
1423
+ try:
1424
+ with open(local_md, "r", encoding="utf-8") as f:
1425
+ local_sig = _skill_signature(f.read())
1426
+ except Exception:
1427
+ return True
1428
+
1429
+ try:
1430
+ remote = await backend.aread(
1431
+ f"{sandbox_dest}/{skill_name}/SKILL.md", _skip_lazy_sync=True)
1432
+ if not remote or not remote.file_data:
1433
+ return False # 读不到 → 当作不一致,触发重传
1434
+ raw = remote.file_data.get("content", "") or ""
1435
+ # 统一用 _decode_remote_skillmd 处理明文/base64 两种返回
1436
+ content = _decode_remote_skillmd(raw)
1437
+ remote_sig = _skill_signature(content)
1438
+ return bool(remote_sig) and local_sig == remote_sig
1439
+ except Exception:
1440
+ return False
1441
+
1442
+
1369
1443
  async def _sync_skills_to_sandbox(
1370
1444
  skills_dir: str,
1371
1445
  backend: HTTPSandboxBackend,
@@ -1424,21 +1498,11 @@ async def _sync_skills_to_sandbox(
1424
1498
  if remote_content and remote_content.file_data:
1425
1499
  # FileData 是 TypedDict(dict), 用键访问而非属性
1426
1500
  content = remote_content.file_data.get("content", "") or ""
1427
- # 优先直接解 version(明文小文件最快);
1428
- # 解不出再兜底解码 base64——沙箱 /read 对大文件(如 skill-creator 的
1429
- # 34KB SKILL.md)返回 base64, encoding 标志位可能漏标/误标,
1430
- # 若依赖该标志位,漏标时会因找不到 "version:" 行误判为"无版本"
1431
- # → 每次新建 Agent 都全量重传。改为「先解 → 解不出再 b64 兜底」,
1432
- # 不依赖 encoding 字段, 对明文/base64 两种返回都正确。
1433
- version = _parse_skill_version(content)
1434
- if version:
1435
- return version
1436
- try:
1437
- import base64 as _b64
1438
- decoded = _b64.b64decode(content).decode("utf-8", errors="replace")
1439
- return _parse_skill_version(decoded)
1440
- except Exception:
1441
- return ""
1501
+ # 沙箱 /read 对大文件(如 skill-creator 的 34KB SKILL.md)返回 base64,
1502
+ # encoding 标志位可能漏标/误标。统一用 _decode_remote_skillmd
1503
+ # 处理明文/base64 两种返回(不依赖 encoding 字段),再解 version。
1504
+ plain = _decode_remote_skillmd(content)
1505
+ return _parse_skill_version(plain)
1442
1506
  except Exception:
1443
1507
  pass
1444
1508
  return ""
@@ -1456,25 +1520,45 @@ async def _sync_skills_to_sandbox(
1456
1520
  remote_version = remote_version_map.get(skill_name, "")
1457
1521
 
1458
1522
  if remote_version and remote_version == local_version:
1459
- SYLogger.debug(
1460
- f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
1461
- backend._synced_skills.add(skill_name)
1462
- skip_count += 1
1463
- continue
1523
+ # 版本一致仍校验正文关键内容,防止"version 字段已更新但正文未同步"
1524
+ # (历史上出现过沙箱 SKILL.md 版本号新、正文旧的脏数据,导致 agent
1525
+ # 永远读不到新版指令)。校验失败则强制走删除+重传。
1526
+ if not await _verify_skill_content_fresh(
1527
+ backend, sandbox_dest, skill_name, src
1528
+ ):
1529
+ SYLogger.warning(
1530
+ f"[DeepAgent] 技能版本一致但正文不一致,强制重传: {skill_name} "
1531
+ f"v{local_version}")
1532
+ # 落到下方的删除+重传分支
1533
+ else:
1534
+ SYLogger.debug(
1535
+ f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
1536
+ backend._synced_skills.add(skill_name)
1537
+ skip_count += 1
1538
+ continue
1464
1539
 
1465
- # 版本不一致或无版本,删除旧目录(走 /delete,跳过懒同步,毫秒级)
1540
+ # 版本不一致或无版本(或正文校验失败),删除旧目录(走 /delete,跳过懒同步,毫秒级)
1466
1541
  if skill_name in existing_dirs:
1467
- try:
1468
- await backend.adelete(
1469
- f"{sandbox_dest}/{skill_name}",
1470
- recursive=True,
1471
- _skip_lazy_sync=True)
1542
+ deleted_ok = False
1543
+ for _attempt in range(2): # 最多重试一次,确保旧内容真删掉
1544
+ try:
1545
+ await backend.adelete(
1546
+ f"{sandbox_dest}/{skill_name}",
1547
+ recursive=True,
1548
+ _skip_lazy_sync=True)
1549
+ deleted_ok = True
1550
+ break
1551
+ except Exception as e:
1552
+ SYLogger.warning(
1553
+ f"[DeepAgent] 删除旧版技能失败(尝试{_attempt+1}): "
1554
+ f"{skill_name}, {e}")
1555
+ if deleted_ok:
1472
1556
  SYLogger.info(
1473
- f"[DeepAgent] 技能版本不一致,覆盖: {skill_name} "
1557
+ f"[DeepAgent] 技能版本不一致,已删除旧目录: {skill_name} "
1474
1558
  f"({remote_version or '无版本'} → {local_version})")
1475
- except Exception as e:
1476
- SYLogger.warning(
1477
- f"[DeepAgent] 删除旧版技能失败: {skill_name}, {e}")
1559
+ else:
1560
+ SYLogger.error(
1561
+ f"[DeepAgent] 删除旧版技能彻底失败,本次跳过: {skill_name}")
1478
1562
 
1479
1563
  # 全量上传整个技能目录到沙箱(batch_upload 服务端无条件覆盖)
1480
1564
  await backend.async_sync_dirs([(src, f"{sandbox_dest}/{skill_name}")])
@@ -350,15 +350,11 @@ async def _load_one_server(
350
350
  pass
351
351
  return []
352
352
  except asyncio.CancelledError:
353
- # 单飞失败透传的哨兵(_discover_with_cache 失败时唤醒等待者用),按失败处理
354
- SYLogger.warning(
355
- f"[MCP] 服务器 '{config.name}' ({config.server_url}) 发现失败,跳过")
356
- if on_server_failure:
357
- try:
358
- await on_server_failure(config.id, "发现失败")
359
- except Exception:
360
- pass
361
- return []
353
+ # 外层取消(如会话重置 cancel 后台 _mcp_task)→ 必须向上传播,不能吞。
354
+ # 吞掉会让 load_mcp_tools_background 的 `except CancelledError: raise` 守卫失效、
355
+ # 任务被标记为正常完成而非已取消。单飞失败的哨兵是 RuntimeError(走下方 except Exception)
356
+ # 不会进这个分支,故这里只需 raise。
357
+ raise
362
358
  except Exception as e:
363
359
  SYLogger.warning(
364
360
  f"[MCP] 服务器 '{config.name}' ({config.server_url}) 连接失败,跳过: {e}")
@@ -24,9 +24,24 @@ from sycommon.logging.kafka_log import SYLogger
24
24
  # 日期快照功能预留,当前暂不启用
25
25
  _SNAPSHOT_ENABLED = False
26
26
 
27
- # restore_from_minio 的 MinIO 下载并发度(同时下载的文件数上限)。
28
- # 与 _sync_batch(sync→minio 方向)的 batch_size 对齐为 5,避免压垮 MinIO/网络。
29
- _RESTORE_CONCURRENCY = 5
27
+ # restore_from_minio 的 MinIO 下载并发度。
28
+ # 两层信号量:
29
+ # - 单用户层 _RESTORE_PER_USER:每个用户恢复时最多 5 路并发下载(单用户恢复够快);
30
+ # - 全局层 _RESTORE_GLOBAL_CAP:全进程最多 15 路下载,防 N 个用户同时恢复时
31
+ # N×5 路把默认线程池(≈min(32,cpu+4)=12)打满、挤占主请求路径的沙箱操作。
32
+ # 单用户独享 5 槽 + 全局封顶 15,兼顾「单用户快」与「多用户不互相压垮」。
33
+ _RESTORE_PER_USER = 5
34
+ _RESTORE_GLOBAL_CAP = 15
35
+ # 全局信号量惰性创建(绑定到首次调用时的 event loop;单 loop 部署下不变)。
36
+ _RESTORE_DOWNLOAD_SEM: "asyncio.Semaphore | None" = None
37
+
38
+
39
+ def _get_restore_download_sem() -> "asyncio.Semaphore":
40
+ """惰性创建全局下载信号量。"""
41
+ global _RESTORE_DOWNLOAD_SEM
42
+ if _RESTORE_DOWNLOAD_SEM is None:
43
+ _RESTORE_DOWNLOAD_SEM = asyncio.Semaphore(_RESTORE_GLOBAL_CAP)
44
+ return _RESTORE_DOWNLOAD_SEM
30
45
 
31
46
 
32
47
  class MinioSyncService(metaclass=SingletonMeta):
@@ -763,15 +778,18 @@ class MinioSyncService(metaclass=SingletonMeta):
763
778
 
764
779
  SYLogger.info(f"[MinIO] 需恢复 {len(to_restore)} 个文件到沙箱: user={user_id}")
765
780
 
766
- # 4. 并发恢复:MinIO 下载用全局 Semaphore(5) 限流并发,沙箱上传仍按批 batch_upload。
767
- # 旧实现是「批内串行下载 + 批间串行」,N 个文件 ≈ N 次串行 MinIO GET,27 个文件要数十秒。
768
- # 改为 Semaphore(5) 后下载并发,总耗时 ≈ ceil(N/5) 次串行往返,对齐 _sync_batch 的并发度。
781
+ # 4. 并发恢复:两层限流。
782
+ # - per_user_sem(每次 restore 新建,本用户独享 5 槽):单用户恢复够快;
783
+ # - global_sem(进程级 15 槽):多用户同时恢复时全局封顶,防 5 路下载
784
+ # 打满线程池、挤占主请求路径的沙箱操作。
785
+ # 下载协程先抢本用户 5 槽、再抢全局 15 槽,故实际并发 = min(本用户待下载, 全局剩余)。
769
786
  restored = 0
770
- download_sem = asyncio.Semaphore(_RESTORE_CONCURRENCY)
787
+ global_sem = _get_restore_download_sem()
788
+ per_user_sem = asyncio.Semaphore(_RESTORE_PER_USER)
771
789
 
772
790
  async def _download_one(rel_path: str):
773
791
  """下载单个 MinIO 对象到内存,返回 (沙箱绝对路径, 内容) 或 None。"""
774
- async with download_sem:
792
+ async with per_user_sem, global_sem:
775
793
  try:
776
794
  def _download_object(bucket, key):
777
795
  resp = self._client.get_object(bucket, key)
@@ -786,18 +804,18 @@ class MinioSyncService(metaclass=SingletonMeta):
786
804
  )
787
805
  return (f"/{rel_path}", content)
788
806
  except Exception as e:
789
- SYLogger.warning(f"[MinIO] 下载文件失败: {rel_path}, error={e}")
807
+ SYLogger.warning(f"[MinIO] 下载文件失败: {rel_path}, error: {e}")
790
808
  return None
791
809
 
792
- # 全量并发下载( Semaphore 限流),比按批串行快得多
810
+ # 全量并发下载(受两层 Semaphore 限流),比按批串行快得多
793
811
  download_results = await asyncio.gather(
794
812
  *[_download_one(rp) for rp in to_restore]
795
813
  )
796
814
  downloaded = [r for r in download_results if r is not None]
797
815
 
798
816
  # 上传仍走 batch_upload(服务端并发),按 5 个一批发(沙箱端点对超大批量可能限流)
799
- for i in range(0, len(downloaded), _RESTORE_CONCURRENCY):
800
- batch = downloaded[i:i + _RESTORE_CONCURRENCY]
817
+ for i in range(0, len(downloaded), _RESTORE_PER_USER):
818
+ batch = downloaded[i:i + _RESTORE_PER_USER]
801
819
  if not batch:
802
820
  continue
803
821
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.7a29
3
+ Version: 0.2.7a31
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -136,7 +136,7 @@ sycommon/services.py,sha256=iC73h5d5bpNqW2qjRUGODZFhUhcwvKVnKUFLKJlBSq0,25322
136
136
  sycommon/agent/__init__.py,sha256=mxceAeUifQ-DKvWp7ZEJIFlmOCb5wpYHPGQw3rwEN8I,4378
137
137
  sycommon/agent/agent_manager.py,sha256=UhhaekEumT7g4v_Z1UB4jTp13X0n8M8erYaQdkGGWkA,13620
138
138
  sycommon/agent/chat_events.py,sha256=t7qWa6OrIWLfqtd1AnqaVP67QWkn1JxpCBTZYQpoLuM,14226
139
- sycommon/agent/deep_agent.py,sha256=XdKpBC5ik-GHU2PyZ4KOeSF3EB2-A-r0SmAV3XJN7lI,74799
139
+ sycommon/agent/deep_agent.py,sha256=uLqhiBLcNQlmaYcg0L6dreJGKHDJ4XFU7GWjuVioIkY,78708
140
140
  sycommon/agent/multi_agent_team.py,sha256=RFXHB9q5j1_2B-MDZVRTcbXHdEpRIXpAhrm1yroUDYI,30460
141
141
  sycommon/agent/summarization_utils.py,sha256=bqZRFqSekNGlv2mYQPaT_s2QNA5VDTKddi2Va_SkHWc,18376
142
142
  sycommon/agent/acp/__init__.py,sha256=vQGMQYAA5-ZaejYmDZ4r4Fklqs17qH3cDXQ5pZlwj-0,1844
@@ -146,7 +146,7 @@ sycommon/agent/acp/ssh_init.py,sha256=5y1kIyNHpsh9ayx9RWO5gS0GOOn2GfZpU4fNrGuhOY
146
146
  sycommon/agent/acp/tool.py,sha256=yt6ohKL2y7wIZMNiLv-m5oCg5u1p5GAbqgjzQVt285Y,10975
147
147
  sycommon/agent/mcp/__init__.py,sha256=iKrdDhIrFsNIkqG_kgcwNe-nOiM6uVfolKv44LfQ-FQ,636
148
148
  sycommon/agent/mcp/models.py,sha256=zda4ho-UTYOJ5oPAZPZ-vxhfDRLghUCsMroHnA2A2QQ,1490
149
- sycommon/agent/mcp/tool_loader.py,sha256=B0dCaRKm2WGGsK6d9TIp4zmuun6QxPwXCVFY7drKEkA,17824
149
+ sycommon/agent/mcp/tool_loader.py,sha256=pwhZoSoZuQVYo6BgfdMoRHlGnRPDRxvOvxEgvUM5q7Q,17818
150
150
  sycommon/agent/middleware/sandbox_path_guard.py,sha256=K1SnxA8pDxkCSucOkranwUvC9_8rd3l_-TIR8gMMTK8,8102
151
151
  sycommon/agent/middleware/sensitive_guard.py,sha256=8e7qSAoak0MNB5fRLscQF0PVGZ07SKdwmGk590GubOI,22870
152
152
  sycommon/agent/middleware/sitecustomize.py,sha256=Bt7XFnWV_LJo89l858AGp5VUkXwefpLGmjkCtLe-CMw,4800
@@ -157,7 +157,7 @@ sycommon/agent/sandbox/__init__.py,sha256=jR7LlkD4J4Y6QYyRXQClkwmqDBCCPmycV_hQV9
157
157
  sycommon/agent/sandbox/consistent_hash.py,sha256=8Jgk-W4NAD2-u5_vKRVlPmql_e0Vy4aNORiXOuzvIrs,7277
158
158
  sycommon/agent/sandbox/file_ops.py,sha256=VTK7xduMMWLVTr9JViMnmPeh7s8Ojr8YwYA4ovMZZg0,37138
159
159
  sycommon/agent/sandbox/http_sandbox_backend.py,sha256=1Cj_JYdvwP3PFPa_4xFPnrGdt67Wp5iQdoziSzjeqoE,70481
160
- sycommon/agent/sandbox/minio_sync.py,sha256=c2TQmnawlpfTyEalSmcIRSK4DNkrZluq-9SjoySrf-M,34515
160
+ sycommon/agent/sandbox/minio_sync.py,sha256=zloxzaoNhEgoiz43NcBalHC_LMd74BoBpr5rN_UfmDA,35432
161
161
  sycommon/agent/sandbox/sandbox_pool.py,sha256=eMn8sLakCWf90l6ni2-333QM8oBdX1CflV-WzneFp_k,9133
162
162
  sycommon/agent/sandbox/sandbox_recovery.py,sha256=X-eDODx1tmGMh_iTngV6e1ppfDBHpTdkPreJusN5MHY,7358
163
163
  sycommon/agent/sandbox/session.py,sha256=TjzC3yFC-VaJ75UwCyL26QX4PRTGNNfQae1FKFuOsYI,2365
@@ -308,8 +308,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
308
308
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
309
309
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
310
310
  sycommon/xxljob/xxljob_service.py,sha256=1yifwIBNGsCIxLnQjHKiBlbsigc_zvPH-dMTZcNxe-Q,7649
311
- sycommon_python_lib-0.2.7a29.dist-info/METADATA,sha256=gq42NKV6-MSlGgQftI3YmU0DiUDErLKMZZOEbMtrOGQ,7962
312
- sycommon_python_lib-0.2.7a29.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
313
- sycommon_python_lib-0.2.7a29.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
314
- sycommon_python_lib-0.2.7a29.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
315
- sycommon_python_lib-0.2.7a29.dist-info/RECORD,,
311
+ sycommon_python_lib-0.2.7a31.dist-info/METADATA,sha256=Tk61Oi4TEDQ8Slxh92K3AJzTEiKvtdIfo61T9TU3k8A,7962
312
+ sycommon_python_lib-0.2.7a31.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
313
+ sycommon_python_lib-0.2.7a31.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
314
+ sycommon_python_lib-0.2.7a31.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
315
+ sycommon_python_lib-0.2.7a31.dist-info/RECORD,,