sycommon-python-lib 0.2.7a6__py3-none-any.whl → 0.2.7a8__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.
@@ -44,7 +44,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, TYPE_CHECKI
44
44
 
45
45
  import yaml
46
46
 
47
- from pydantic import BaseModel, Field
47
+ from pydantic import BaseModel, Field, model_validator
48
48
  from langchain_core.messages import HumanMessage, BaseMessage, AIMessageChunk, AIMessage, SystemMessage
49
49
  from langchain_core.tools import BaseTool, tool
50
50
 
@@ -57,6 +57,7 @@ from sycommon.agent.sandbox.sandbox_recovery import SandboxRecoveryManager
57
57
  from sycommon.agent.chat_events import ChatEvent, ChatEventBuilder, DEFAULT_AGENT_NAME
58
58
  from sycommon.middleware.token_tracking import TokenTrackingMiddleware
59
59
  from sycommon.middleware.tool_result_truncation import ToolResultTruncationMiddleware
60
+ from sycommon.agent.middleware.skill_write_guard import SkillWriteGuardMiddleware
60
61
  from deepagents.middleware.summarization import create_summarization_tool_middleware # noqa: F401 保留 re-export
61
62
  from sycommon.agent.summarization_utils import build_summarization_middleware
62
63
 
@@ -373,13 +374,42 @@ class AgentConfig(BaseModel):
373
374
  # 沙箱配置
374
375
  sandbox_service_name: str = "shengye-platform-sandbox"
375
376
  sandbox_timeout: int = 300
376
- skills_dir: Optional[str] = None
377
- # 技能是否已同步到沙箱:True 时跳过 _sync_skills_to_sandbox 的 atree 检查,
378
- # 但仍会基于 skills_dir 注入 /skills(即 SkillsMiddleware 照常挂载)。
379
- # 注意:不要用「把 skills_dir 置 None」来跳过同步——那会连带关闭技能注入。
377
+ # system 技能:本地仓库目录路径 → 同步到沙箱 /skills/system(只读)
378
+ system_skills_dir: Optional[str] = None
379
+ # user 技能:标记启用沙箱 /skills/user(可写,内容由 MinIO 按 user 灌入);
380
+ # 本地兜底路径仅当目录存在时用于开发期同步,生产期为 None
381
+ user_skills_dir: Optional[str] = None
382
+ # 技能是否已同步到沙箱:True 时跳过 _sync_skills_to_sandbox 的 atree 检查,
383
+ # 但 skills 注入(SkillsMiddleware)不受影响。
380
384
  skills_already_synced: bool = False
381
385
  memory_dir: Optional[str] = None
382
386
 
387
+ @property
388
+ def skills_dir(self) -> Optional[str]:
389
+ """兼容旧字段:取 system_skills_dir(迁移期防外部调用崩)。"""
390
+ return self.system_skills_dir
391
+
392
+ @model_validator(mode="before")
393
+ @classmethod
394
+ def _map_legacy_skills_dir(cls, data: Any) -> Any:
395
+ """旧 skills_dir=... 映射到 system_skills_dir 并告警(修 I3)。
396
+
397
+ pydantic v2 默认 ignore 未知字段,而 skills_dir 现在是只读 property
398
+ (非字段)——直接传 skills_dir=... 会被静默丢弃,导致 agent 无技能却无报错。
399
+ 这里在解析前拦截:若仍用旧 kwarg,映射到 system_skills_dir + DeprecationWarning。
400
+ """
401
+ if isinstance(data, dict) and "skills_dir" in data \
402
+ and "system_skills_dir" not in data:
403
+ import warnings
404
+ warnings.warn(
405
+ "AgentConfig(skills_dir=...) 已弃用,请改用 system_skills_dir="
406
+ "(system 只读)+ user_skills_dir=(user 可写)。本次按 system 处理。",
407
+ DeprecationWarning,
408
+ stacklevel=2,
409
+ )
410
+ data = {**data, "system_skills_dir": data.pop("skills_dir")}
411
+ return data
412
+
383
413
  # 工具
384
414
  tools: List[BaseTool] = Field(default_factory=list)
385
415
 
@@ -599,8 +629,10 @@ class DeepAgent:
599
629
  args_str = str(tc_args_l)[:100]
600
630
  SYLogger.debug(
601
631
  f"[DeepAgent #{stream_step}] tool_call | {tc_l.get('name','?')} | args={args_str}")
602
- elif not (msg.content or ""):
603
- # 检查是否有 reasoning_content(思考模式下内容在 reasoning_content 而非 content)
632
+ elif _STREAM_DEBUG and not (msg.content or ""):
633
+ # 仅在 DEEP_AGENT_STREAM_DEBUG=true 时打印逐 chunk 诊断日志。
634
+ # 生产环境默认关闭:思考模式下每 token 都会触发此分支,
635
+ # "(empty)"/"reasoning_content" 行数会随回答长度线性增长,刷爆日志量。
604
636
  _rc = (
605
637
  getattr(msg, "reasoning_content", None)
606
638
  or getattr(msg, "additional_kwargs", {}).get("reasoning_content", None)
@@ -1163,17 +1195,16 @@ async def create_deep_agent(
1163
1195
  tid = thread_id or user_id
1164
1196
  agent_config = {"configurable": {"thread_id": tid}}
1165
1197
 
1166
- # 确定沙箱路径
1167
- # skills_dir: 本地目录路径 注入沙箱 /skills(SkillsMiddleware 据此挂载)
1168
- # memory_dir: 非路径字符串 标记启用沙箱 /memory/AGENTS.md
1169
- skills_path = "/skills" if config.skills_dir else None
1198
+ # 沙箱 skills 双层结构:/skills/system(只读,仓库源)+ /skills/user(可写,MinIO 按 user 灌入)
1199
+ system_path = "/skills/system" if config.system_skills_dir else None
1200
+ user_path = "/skills/user" if config.user_skills_dir else None
1170
1201
  memory_path = "/memory/AGENTS.md" if config.memory_dir else None
1171
1202
 
1172
- # 同步 skills 到沙箱(按目录名检查,已存在则跳过)
1173
- # 仅在未同步过时执行 atree 检查 + 上传;skills_already_synced=True 时跳过同步,
1174
- # skills_path 注入不受影响,确保 SkillsMiddleware 始终能挂载。
1175
- if config.skills_dir and sandbox_backend and not config.skills_already_synced:
1176
- await _sync_skills_to_sandbox(config.skills_dir, sandbox_backend)
1203
+ # 同步 system 技能到沙箱(按版本检查,已同步则跳过)+ chmod 只读保护
1204
+ # skills_already_synced=True 时跳过同步,但 system_path/user_path 注入不受影响。
1205
+ if config.system_skills_dir and sandbox_backend and not config.skills_already_synced:
1206
+ await _sync_skills_to_sandbox(config.system_skills_dir, sandbox_backend,
1207
+ sandbox_dest="/skills/system", chmod_readonly=True)
1177
1208
 
1178
1209
  # 创建 agent
1179
1210
  from deepagents import create_deep_agent as _create_deep_agent
@@ -1194,6 +1225,7 @@ async def create_deep_agent(
1194
1225
  ToolResultTruncationMiddleware(),
1195
1226
  TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
1196
1227
  summarization_mw,
1228
+ SkillWriteGuardMiddleware(), # 系统 /skills/system 只读守卫(先于白名单)
1197
1229
  ]
1198
1230
 
1199
1231
  # RubricMiddleware:复用 FilesystemMiddleware 的工具(底层走 HTTPSandboxBackend HTTP 远程调用)
@@ -1226,8 +1258,15 @@ async def create_deep_agent(
1226
1258
  "debug": config.debug,
1227
1259
  "middleware": middleware_list,
1228
1260
  }
1229
- if skills_path:
1230
- agent_kwargs["skills"] = [skills_path]
1261
+ # deepagents SkillsMiddleware 支持多 source (path, label),后 source 覆盖先 source
1262
+ # user 在后,user 同名技能优先于 system
1263
+ _sources = []
1264
+ if system_path:
1265
+ _sources.append((system_path, "系统"))
1266
+ if user_path:
1267
+ _sources.append((user_path, "用户"))
1268
+ if _sources:
1269
+ agent_kwargs["skills"] = _sources
1231
1270
  if memory_path:
1232
1271
  agent_kwargs["memory"] = [memory_path]
1233
1272
 
@@ -1297,21 +1336,24 @@ def _parse_skill_version(content: str) -> str:
1297
1336
  async def _sync_skills_to_sandbox(
1298
1337
  skills_dir: str,
1299
1338
  backend: HTTPSandboxBackend,
1339
+ *,
1340
+ sandbox_dest: str = "/skills",
1341
+ chmod_readonly: bool = False,
1300
1342
  ) -> None:
1301
- """同步技能目录到沙箱(按版本检查,版本不一致则全量覆盖)
1343
+ """同步技能目录到沙箱(按版本检查,版本不一致则全量覆盖)
1302
1344
 
1303
- 技能来源是仓库内的 skills/ 目录(随部署持久,不随 Pod 重启丢失),
1304
- 版本比较只看沙箱 /skills/{name}/SKILL.md,无需 MinIO 锚点:
1345
+ 技能来源是仓库内的 skills/ 目录(随部署持久,不随 Pod 重启丢失),
1346
+ 版本比较只看沙箱 sandbox_dest/{name}/SKILL.md,无需 MinIO 锚点:
1305
1347
  - 沙箱有该技能且 version 一致 → 跳过
1306
- - 沙箱没有 / version 不一致 → 从本地 skills/ 全量上传到沙箱
1348
+ - 沙箱没有 / 版本不一致 → 从本地 skills/ 全量上传到沙箱
1307
1349
  """
1308
1350
  if not os.path.isdir(skills_dir):
1309
1351
  return
1310
1352
 
1311
1353
  try:
1312
- # 获取沙箱 /skills 下已有的目录列表
1354
+ # 获取沙箱 sandbox_dest 下已有的目录列表
1313
1355
  existing_dirs = set()
1314
- tree_result = await backend.atree("/skills", max_depth=1)
1356
+ tree_result = await backend.atree(sandbox_dest, max_depth=1)
1315
1357
  if tree_result and tree_result.tree and tree_result.tree.children:
1316
1358
  for child in tree_result.tree.children:
1317
1359
  if child.is_dir:
@@ -1334,12 +1376,12 @@ async def _sync_skills_to_sandbox(
1334
1376
  except Exception:
1335
1377
  pass
1336
1378
 
1337
- # 比较版本:只从沙箱 /skills/{name}/SKILL.md 读取
1379
+ # 比较版本:只从沙箱 sandbox_dest/{name}/SKILL.md 读取
1338
1380
  remote_version = ""
1339
1381
  if skill_name in existing_dirs:
1340
1382
  try:
1341
1383
  remote_content = await backend.aread(
1342
- f"/skills/{skill_name}/SKILL.md")
1384
+ f"{sandbox_dest}/{skill_name}/SKILL.md")
1343
1385
  if remote_content:
1344
1386
  remote_version = _parse_skill_version(remote_content)
1345
1387
  except Exception:
@@ -1347,29 +1389,39 @@ async def _sync_skills_to_sandbox(
1347
1389
 
1348
1390
  if remote_version and remote_version == local_version:
1349
1391
  SYLogger.debug(
1350
- f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
1392
+ f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
1351
1393
  backend._synced_skills.add(skill_name)
1352
1394
  skip_count += 1
1353
1395
  continue
1354
1396
 
1355
- # 版本不一致或无版本,删除旧目录后全量覆盖
1397
+ # 版本不一致或无版本,删除旧目录后全量覆盖
1356
1398
  if skill_name in existing_dirs:
1357
1399
  try:
1358
- await backend.aexecute(f"rm -rf /skills/{skill_name}")
1400
+ await backend.aexecute(f"rm -rf {sandbox_dest}/{skill_name}")
1359
1401
  SYLogger.info(
1360
- f"[DeepAgent] 技能版本不一致,覆盖: {skill_name} "
1402
+ f"[DeepAgent] 技能版本不一致,覆盖: {skill_name} "
1361
1403
  f"({remote_version or '无版本'} → {local_version})")
1362
1404
  except Exception as e:
1363
1405
  SYLogger.warning(
1364
1406
  f"[DeepAgent] 删除旧版技能失败: {skill_name}, {e}")
1365
1407
 
1366
1408
  # 全量上传整个技能目录到沙箱
1367
- await backend.async_sync_dirs([(src, f"/skills/{skill_name}")])
1409
+ await backend.async_sync_dirs([(src, f"{sandbox_dest}/{skill_name}")])
1368
1410
  backend._synced_skills.add(skill_name)
1369
1411
  skill_count += 1
1370
1412
  SYLogger.info(
1371
1413
  f"[DeepAgent] 全量同步技能到沙箱: {skill_name} v{local_version}")
1372
1414
 
1415
+ if chmod_readonly:
1416
+ try:
1417
+ # sandbox_dest 是虚拟绝对路径(/skills/system),沙箱 shell 不重映射裸绝对路径,
1418
+ # 须拼 $_SANDBOX_WORKSPACE 指向真实 workspace 才能命中。否则 chmod 静默失败。
1419
+ await backend.aexecute(
1420
+ f'chmod -R 555 "$_SANDBOX_WORKSPACE{sandbox_dest}"')
1421
+ SYLogger.info(f"[DeepAgent] 已设只读: chmod -R 555 {sandbox_dest}")
1422
+ except Exception as e:
1423
+ SYLogger.warning(f"[DeepAgent] chmod 只读失败(忽略): {e}")
1424
+
1373
1425
  SYLogger.info(
1374
1426
  f"[DeepAgent] 技能同步完成: 同步 {skill_count} 个, 跳过 {skip_count} 个")
1375
1427
  except Exception as e:
@@ -1396,9 +1448,11 @@ async def _create_sandbox_backend(
1396
1448
  memory_dir.mkdir(parents=True, exist_ok=True)
1397
1449
  sync_dirs.append((str(memory_dir), "/memory"))
1398
1450
 
1399
- # 确定 skills_dir,传递给沙箱后端用于懒同步
1400
- if config.skills_dir:
1401
- skills_dir_for_backend = str(Path(config.skills_dir))
1451
+ # 确定 skills_dir,传递给沙箱后端用于懒同步。
1452
+ # 懒同步按 os.path.join(_skills_dir, scope, skill_name) 解析本地源(system/user/<域>),
1453
+ # _skills_dir 须是 system/ 与 user/ 的【父目录】(<repo>/skills),而非 system_skills_dir 本身。
1454
+ if config.system_skills_dir:
1455
+ skills_dir_for_backend = str(Path(config.system_skills_dir).parent)
1402
1456
  else:
1403
1457
  skills_dir_for_backend = str(project_root / "skills")
1404
1458
 
@@ -30,15 +30,13 @@ from langgraph.prebuilt.tool_node import ToolCallRequest
30
30
 
31
31
  from sycommon.logging.kafka_log import SYLogger
32
32
 
33
- # 命令路径形如 skills/sy-oa-skill/workflow-todo/scripts/x.py
34
- # skills/sy-syt-skill/shengye-platform-zhongdeng/SKILL.md
35
- # skills/sy-syt-skill/generate-plan/... (有/无 scripts/ 都识别)
36
- # 取「skills/<顶级技能>/」的【第一段】作顶级技能名(如 sy-oa-skill),
37
- # 配合 SkillApiWhitelist 按顶级技能(5 个域)配置——每个域的 allowlist 是其下
38
- # 所有子技能出网地址的并集。用 (?:/|$) 锚定段尾,避免误吞后续路径段。
39
- # (?<![\w]) 否定左查找:要求 skills 前不是单词字符,避免误匹配 myskills/、
40
- # theskills/ 等含 skills 子串的路径(行首、空格、引号、/ 后均成立)。
41
- _SKILL_TOP_RE = re.compile(r'(?<![\w])skills/([A-Za-z0-9_\-]+)(?:/|$)')
33
+ # 命令路径形如 skills/system/sy-oa-skill/workflow-todo/scripts/x.py
34
+ # skills/user/sy-oa-skill/SKILL.md
35
+ # 拆为 system/user 双层后,顶层段是 system|user,需跳过它取【第二段】作域级技能名
36
+ # (如 sy-oa-skill),配合 SkillApiWhitelist 按顶级域配置。
37
+ # (?<![\w]) 否定左查找:要求 skills 前不是单词字符,避免误匹配 myskills/。
38
+ _SKILL_TOP_RE = re.compile(
39
+ r'(?<![\w])skills/(?:system|user)/([A-Za-z0-9_\-]+)(?:/|$)')
42
40
 
43
41
  # BASE_URL 占位符 ${VAR}
44
42
  _PLACEHOLDER_RE = re.compile(r'\$\{([A-Z_][A-Z0-9_]*)\}')
@@ -0,0 +1,61 @@
1
+ """系统技能只读守卫中间件。
2
+
3
+ 拦 write_file / edit_file / execute 三类写操作对 /skills/system 的命中,
4
+ 返回 error ToolMessage(不调 handler,短路)。/skills/user 不拦。
5
+
6
+ 双层防护之一(另一层是沙箱 chmod -R 555 /skills/system):
7
+ 本中间件负责给 agent 清晰错误反馈并 fail-closed,chmod 负责兜底防 shell 绕过。
8
+ """
9
+ import re
10
+ from langchain.agents.middleware.types import AgentMiddleware
11
+ from langchain_core.messages import ToolMessage
12
+ from langgraph.prebuilt.tool_node import ToolCallRequest
13
+
14
+ # /skills/system 或 /skills/system/... (行尾/引号/空白/斜杠均可作边界,防匹配 /skills/systembackup)
15
+ _PROTECTED_PATH_RE = re.compile(r'/skills/system(?:/|\s|"|\'|$)')
16
+
17
+ # 写动作动词(命令首或分号/管道/空格后)
18
+ _WRITE_VERB_RE = re.compile(
19
+ r'(^|[\s;&|])(rm|rmdir|mv|cp|chmod|chown|tee|truncate|install|rsync|scp|dd)\b')
20
+ # 输出重定向 > 或 >>
21
+ _REDIRECT_RE = re.compile(r'>>?')
22
+
23
+ _WRITE_TOOLS = {"write_file", "edit_file"}
24
+ _DENIED_MSG = ("Error: 系统技能 /skills/system 为只读,禁止修改/删除。"
25
+ "如需定制,请复制到用户技能 /skills/user/ 后编辑。")
26
+
27
+
28
+ def _is_protected_path(p: str) -> bool:
29
+ s = (p or "").strip().replace("\\", "/")
30
+ return s == "/skills/system" or s.startswith("/skills/system/")
31
+
32
+
33
+ def _command_writes_system(cmd: str) -> bool:
34
+ if not _PROTECTED_PATH_RE.search(cmd or ""):
35
+ return False
36
+ return bool(_WRITE_VERB_RE.search(cmd) or _REDIRECT_RE.search(cmd))
37
+
38
+
39
+ class SkillWriteGuardMiddleware(AgentMiddleware):
40
+ """拦对 /skills/system 的写。仅实现 awrap_tool_call(异步链路)。"""
41
+
42
+ async def awrap_tool_call(self, request: ToolCallRequest, handler):
43
+ tc = request.tool_call
44
+ name = tc.get("name")
45
+ args = tc.get("args", {}) or {}
46
+
47
+ if name in _WRITE_TOOLS:
48
+ fp = args.get("file_path") if isinstance(args, dict) else ""
49
+ if _is_protected_path(fp):
50
+ return ToolMessage(content=_DENIED_MSG, name=name,
51
+ tool_call_id=tc.get("id", ""), status="error")
52
+ return await handler(request)
53
+
54
+ if name == "execute":
55
+ command = args.get("command", "") if isinstance(args, dict) else ""
56
+ if _command_writes_system(command):
57
+ return ToolMessage(content=_DENIED_MSG, name="execute",
58
+ tool_call_id=tc.get("id", ""), status="error")
59
+ return await handler(request)
60
+
61
+ return await handler(request)
@@ -45,6 +45,7 @@ from sycommon.agent.sandbox.sandbox_recovery import SandboxRecoveryManager
45
45
  from sycommon.agent.chat_events import ChatEvent, ChatEventBuilder, DEFAULT_AGENT_NAME
46
46
  from sycommon.middleware.token_tracking import TokenTrackingMiddleware
47
47
  from sycommon.middleware.tool_result_truncation import ToolResultTruncationMiddleware
48
+ from sycommon.agent.middleware.skill_write_guard import SkillWriteGuardMiddleware
48
49
  from deepagents.middleware.summarization import create_summarization_tool_middleware # noqa: F401 保留 re-export
49
50
  from sycommon.agent.summarization_utils import build_summarization_middleware
50
51
 
@@ -131,9 +132,19 @@ class TeamConfig(BaseModel):
131
132
 
132
133
  sandbox_service_name: str = "shengye-platform-sandbox"
133
134
  sandbox_timeout: int = 300
134
- skills_dir: Optional[str] = None
135
+ # system 技能:本地仓库目录 → 同步到沙箱 /skills/system(只读)
136
+ system_skills_dir: Optional[str] = None
137
+ # user 技能:标记启用沙箱 /skills/user(可写,内容由 MinIO 灌入)
138
+ user_skills_dir: Optional[str] = None
139
+ # 技能是否已同步到沙箱:True 时跳过 _sync_skills_to_sandbox
140
+ skills_already_synced: bool = False
135
141
  memory_dir: Optional[str] = None
136
142
 
143
+ @property
144
+ def skills_dir(self) -> Optional[str]:
145
+ """兼容旧字段:取 system_skills_dir。"""
146
+ return self.system_skills_dir
147
+
137
148
  shared_tools: List[BaseTool] = Field(default_factory=list)
138
149
 
139
150
  debug: bool = False
@@ -578,6 +589,21 @@ async def create_multi_agent_team(
578
589
  tid = thread_id or user_id
579
590
  agent_config = {"configurable": {"thread_id": tid}}
580
591
 
592
+ # 同步 system 技能到沙箱(按版本检查)+ chmod 只读保护(与单 Agent 路径对齐)
593
+ if config.system_skills_dir and sandbox_backend and not config.skills_already_synced:
594
+ from sycommon.agent.deep_agent import _sync_skills_to_sandbox
595
+ await _sync_skills_to_sandbox(config.system_skills_dir, sandbox_backend,
596
+ sandbox_dest="/skills/system", chmod_readonly=True)
597
+
598
+ # 沙箱 skills 双层路径(用于 SkillsMiddleware 双 source)
599
+ system_path = "/skills/system" if config.system_skills_dir else None
600
+ user_path = "/skills/user" if config.user_skills_dir else None
601
+ skill_sources = []
602
+ if system_path:
603
+ skill_sources.append((system_path, "系统"))
604
+ if user_path:
605
+ skill_sources.append((user_path, "用户")) # user 在后 → 同名优先
606
+
581
607
  # 创建子 agents(递归支持嵌套团队)
582
608
  from deepagents import create_deep_agent
583
609
  from deepagents.middleware.subagents import CompiledSubAgent
@@ -587,6 +613,7 @@ async def create_multi_agent_team(
587
613
  ToolResultTruncationMiddleware(),
588
614
  TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
589
615
  summarization_mw,
616
+ SkillWriteGuardMiddleware(),
590
617
  ]
591
618
  shared = config.shared_tools or [get_current_date]
592
619
 
@@ -632,7 +659,7 @@ async def create_multi_agent_team(
632
659
  model=model,
633
660
  tools=config.shared_tools or [get_current_date],
634
661
  backend=sandbox_backend,
635
- skills=["/skills"],
662
+ skills=skill_sources or None,
636
663
  memory=["/memory/AGENTS.md"],
637
664
  checkpointer=checkpointer,
638
665
  system_prompt=config.coordinator_prompt,
@@ -643,6 +670,7 @@ async def create_multi_agent_team(
643
670
  ToolResultTruncationMiddleware(),
644
671
  TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
645
672
  coord_summarization_mw,
673
+ SkillWriteGuardMiddleware(),
646
674
  ],
647
675
  )
648
676
 
@@ -679,9 +707,10 @@ async def _create_sandbox_backend_for_team(
679
707
  memory_dir.mkdir(parents=True, exist_ok=True)
680
708
  sync_dirs.append((str(memory_dir), "/memory"))
681
709
 
682
- # 确定 skills_dir,传递给沙箱后端用于懒同步
683
- if config.skills_dir:
684
- skills_dir_for_backend = str(Path(config.skills_dir))
710
+ # 懒同步按 os.path.join(_skills_dir, scope, skill_name) 解析本地源(system/<域>),
711
+ # 故 _skills_dir 须是 system/ 与 user/ 的【父目录】。
712
+ if config.system_skills_dir:
713
+ skills_dir_for_backend = str(Path(config.system_skills_dir).parent)
685
714
  else:
686
715
  skills_dir_for_backend = str(project_root / "skills")
687
716
 
@@ -9,6 +9,7 @@ HTTP 客户端沙箱后端
9
9
  import asyncio
10
10
  import base64
11
11
  import os
12
+ import re
12
13
  import ssl
13
14
  import sys
14
15
  import tempfile
@@ -36,6 +37,12 @@ try:
36
37
  except ImportError:
37
38
  _SSL_CONTEXT = ssl.create_default_context()
38
39
 
40
+ # 技能访问路径解析:/skills/(system|user)/<域>/...
41
+ # 前导斜杠可选(命令里可能是 cd skills/system/x),(?<![\w]) 防误匹配 myskills/
42
+ _SKILL_PATH_RE = re.compile(
43
+ r'(?:^|[\s/\'"])skills/(system|user)/([a-zA-Z0-9_\-]+)/'
44
+ )
45
+
39
46
 
40
47
  def _read_file_safe(path: str) -> bytes:
41
48
  with open(path, "rb") as f:
@@ -851,33 +858,29 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
851
858
  if not self._skills_dir:
852
859
  return
853
860
 
854
- import re
855
- # 匹配 /skills/<skill_name>/ 或 skills/<skill_name>/
856
- match = re.search(r'/skills/([a-zA-Z0-9_\-]+)/', path)
857
- if not match:
858
- match = re.search(r'(?:^|\s|")skills/([a-zA-Z0-9_\-]+)/', path)
861
+ match = _SKILL_PATH_RE.search(path)
859
862
  if not match:
860
863
  return
861
-
862
- skill_name = match.group(1)
863
- if skill_name in self._synced_skills:
864
+ scope, skill_name = match.group(1), match.group(2)
865
+ cache_key = f"{scope}/{skill_name}"
866
+ if cache_key in self._synced_skills:
864
867
  return
865
868
 
866
- skill_dir = os.path.join(self._skills_dir, skill_name)
869
+ skill_dir = os.path.join(self._skills_dir, scope, skill_name)
867
870
  if not os.path.isdir(skill_dir):
868
- self._synced_skills.add(skill_name)
871
+ self._synced_skills.add(cache_key)
869
872
  return
870
873
 
871
874
  try:
872
875
  await self.async_sync_dirs([
873
- (skill_dir, f"/skills/{skill_name}")
876
+ (skill_dir, f"/skills/{scope}/{skill_name}")
874
877
  ])
875
- self._synced_skills.add(skill_name)
878
+ self._synced_skills.add(cache_key)
876
879
  SYLogger.info(
877
- f"[Sandbox] 懒同步技能目录: {skill_name}/")
880
+ f"[Sandbox] 懒同步技能目录: {cache_key}/")
878
881
  except Exception as e:
879
882
  SYLogger.warning(
880
- f"[Sandbox] 懒同步技能目录失败: {skill_name}, {e}")
883
+ f"[Sandbox] 懒同步技能目录失败: {cache_key}, {e}")
881
884
 
882
885
  def _lazy_sync_skill_from_path_sync(self, path: str) -> None:
883
886
  """同步版本的懒同步(在同步文件操作中调用)
@@ -887,32 +890,29 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
887
890
  if not self._skills_dir:
888
891
  return
889
892
 
890
- import re
891
- match = re.search(r'/skills/([a-zA-Z0-9_\-]+)/', path)
892
- if not match:
893
- match = re.search(r'(?:^|\s|")skills/([a-zA-Z0-9_\-]+)/', path)
893
+ match = _SKILL_PATH_RE.search(path)
894
894
  if not match:
895
895
  return
896
-
897
- skill_name = match.group(1)
898
- if skill_name in self._synced_skills:
896
+ scope, skill_name = match.group(1), match.group(2)
897
+ cache_key = f"{scope}/{skill_name}"
898
+ if cache_key in self._synced_skills:
899
899
  return
900
900
 
901
- skill_dir = os.path.join(self._skills_dir, skill_name)
901
+ skill_dir = os.path.join(self._skills_dir, scope, skill_name)
902
902
  if not os.path.isdir(skill_dir):
903
- self._synced_skills.add(skill_name)
903
+ self._synced_skills.add(cache_key)
904
904
  return
905
905
 
906
906
  try:
907
907
  # 同步版本直接调用同步上传方法
908
908
  self._upload_directory_sync(
909
- skill_dir, f"/skills/{skill_name}", mode="full")
910
- self._synced_skills.add(skill_name)
909
+ skill_dir, f"/skills/{scope}/{skill_name}", mode="full")
910
+ self._synced_skills.add(cache_key)
911
911
  SYLogger.info(
912
- f"[Sandbox] 懒同步技能目录(sync): {skill_name}/")
912
+ f"[Sandbox] 懒同步技能目录(sync): {cache_key}/")
913
913
  except Exception as e:
914
914
  SYLogger.warning(
915
- f"[Sandbox] 懒同步技能目录失败(sync): {skill_name}, {e}")
915
+ f"[Sandbox] 懒同步技能目录失败(sync): {cache_key}, {e}")
916
916
 
917
917
  async def _sync_async(self, *, timeout: int = None) -> dict:
918
918
  """同步目录(异步版本)
@@ -326,6 +326,20 @@ class MinioSyncService(metaclass=SingletonMeta):
326
326
  else:
327
327
  files.append(path)
328
328
 
329
+ def _exclude_system_skills(self, files: list[str]) -> list[str]:
330
+ """过滤掉 /skills/system/ 前缀文件——system 是只读仓库产物,不进 MinIO。
331
+
332
+ 路径形如 skills/system/sy-oa-skill/...(collect_sandbox_files 已 strip 根 user_id
333
+ 前缀,根盘符 / 由调用方拼接)。兼容带不带前导斜杠。
334
+ """
335
+ out = []
336
+ for fp in files:
337
+ norm = fp.replace("\\", "/").lstrip("/")
338
+ if norm == "skills/system" or norm.startswith("skills/system/"):
339
+ continue
340
+ out.append(fp)
341
+ return out
342
+
329
343
  async def sync_sandbox_to_minio(self, sandbox_backend, user_id: str) -> int:
330
344
  """扫描沙箱目录,增量同步文件到 MinIO
331
345
 
@@ -357,6 +371,7 @@ class MinioSyncService(metaclass=SingletonMeta):
357
371
  return 0
358
372
 
359
373
  sandbox_files = self.collect_sandbox_files(tree_result)
374
+ sandbox_files = self._exclude_system_skills(sandbox_files)
360
375
  if not sandbox_files:
361
376
  # 沙箱为空,清理 MinIO 中该用户的所有文件
362
377
  try:
@@ -523,6 +538,11 @@ class MinioSyncService(metaclass=SingletonMeta):
523
538
  if rel_path and rel_path not in existing_files:
524
539
  to_restore.append(rel_path)
525
540
 
541
+ # 排除 /skills/system —— system 是只读仓库产物,由本地仓库注入,
542
+ # 绝不从 MinIO 恢复(否则旧的/遗留 system 副本会落回用户沙箱,污染只读区)。
543
+ # 与 sync_sandbox_to_minio 的排除对称,守住「system 不进 MinIO」不变式。
544
+ to_restore = self._exclude_system_skills(to_restore)
545
+
526
546
  if not to_restore:
527
547
  SYLogger.info(f"[MinIO] 沙箱文件完整,无需恢复: user={user_id}")
528
548
  return 0
@@ -206,10 +206,9 @@ async def _patched_awrap_model_call(self, request, handler):
206
206
  real_tokens = _extract_last_usage_total_tokens(effective_messages)
207
207
 
208
208
  # 估算值(用于日志对比)
209
- try:
210
- estimated = _approx_counter(counted_messages, tools=request.tools)
211
- except TypeError:
212
- estimated = _approx_counter(counted_messages)
209
+ # langchain-core>=1.4.8 的 count_tokens_approximately 原生支持 tools=,
210
+ # 不再需要 try/except TypeError 兜底。
211
+ estimated = _approx_counter(counted_messages, tools=request.tools)
213
212
 
214
213
  # 如果有真实 token,临时替换 token_counter 使 _orig 内部判断也用真实值
215
214
  # 这样 _should_summarize 和 _determine_cutoff_index 都能拿到正确的 token 数
@@ -301,6 +300,11 @@ except ImportError:
301
300
  # 虽然不能改变 __init__ 的默认参数(Python 在定义时求值),
302
301
  # 但可以防止任何运行时读取 DEFAULT_SUMMARY_PROMPT 的代码拿到英文版。
303
302
  _summ_mod.DEFAULT_SUMMARY_PROMPT = CUSTOM_SUMMARY_PROMPT
303
+ # deepagents 0.6.12 起 SummarizationMiddleware 的默认 prompt 改读
304
+ # DEEPAGENTS_DEFAULT_SUMMARY_PROMPT(在模块加载时由 DEFAULT_SUMMARY_PROMPT 派生),
305
+ # 因此只替换 DEFAULT_SUMMARY_PROMPT 已无法阻断新链路,需同步覆盖派生常量。
306
+ if hasattr(_summ_mod, "DEEPAGENTS_DEFAULT_SUMMARY_PROMPT"):
307
+ _summ_mod.DEEPAGENTS_DEFAULT_SUMMARY_PROMPT = CUSTOM_SUMMARY_PROMPT
304
308
  try:
305
309
  import langchain.agents.middleware.summarization as _lc_summ_mod
306
310
  _lc_summ_mod.DEFAULT_SUMMARY_PROMPT = CUSTOM_SUMMARY_PROMPT
@@ -3,6 +3,7 @@ import sys
3
3
  import json
4
4
  import logging
5
5
  import socket
6
+ import time
6
7
  import threading
7
8
  import traceback
8
9
  import asyncio
@@ -75,6 +76,25 @@ class KafkaSink:
75
76
  self._producer_obj = self._build_producer() if self._enabled else None
76
77
  self._producer = self._producer_obj
77
78
 
79
+ # ===== 自愈计数器(健康探测 + 重建)=====
80
+ # 背景:网络长时间中断时,kafka-python 的投递 future 会持续失败(RequestTimedOut /
81
+ # KafkaTimeoutError 批次过期),即使网络恢复,旧 producer 内部累积的脏批次 / 已关闭的
82
+ # sender 线程 / 满载的 RecordAccumulator 也可能让它在恢复后继续报错(日志里曾出现
83
+ # `Task is already done!`、`Uncaught error in kafka producer I/O thread` + `KeyError
84
+ # muted.remove`,sender 线程一旦抛到这里就不再正常处理)。
85
+ # 做法:累计投递失败数;达到阈值就关闭旧 producer 并重建一个全新的。重建带冷却,避免
86
+ # 网络持续断时疯狂重建。
87
+ # 并发:send() 在 loguru 后台线程调用,future 的 errback 回调在 kafka-python 内部
88
+ # 线程触发,两者都会动这些计数器,必须加锁。
89
+ self._rebuild_lock = threading.Lock()
90
+ self._consecutive_failures = 0 # 连续投递失败计数(成功一次即清零)
91
+ self._last_rebuild_ts = 0.0 # 最近一次重建的时间戳(time.monotonic()),用于冷却
92
+ self._rebuild_in_progress = False # 重建进行中标志:期间 write() 跳过惰性建 producer,避免与重建线程互踩
93
+ # 阈值:连续失败到这个数就触发重建。结合 linger/retries,意味着投递已经持续失败而非偶发抖动。
94
+ self._rebuild_failure_threshold = 50
95
+ # 冷却:两次重建之间至少间隔这么多秒,避免长断网下每条日志都尝试重建(每次重建要建 TCP + bootstrap)
96
+ self._rebuild_cooldown_sec = 60.0
97
+
78
98
  # 缓存主机信息,避免每次日志写入都做 DNS 解析
79
99
  try:
80
100
  self._ip = socket.gethostbyname(socket.gethostname())
@@ -88,15 +108,28 @@ class KafkaSink:
88
108
  注意:kafka-python 的 KafkaProducer 在构造时会尝试拉取元数据,
89
109
  broker 不可达时可能抛异常——此处不吞异常,由调用方决定
90
110
  (__init__ 保持原启动期失败即抛的行为;write() 的惰性路径 try 包裹)。
111
+
112
+ max_block_ms 收紧到 5000ms(原 60000ms):
113
+ kafka-python 在缓冲区满 / 元数据不可用时,send() 会阻塞调用线程直到
114
+ max_block_ms。本 sink 在 loguru 后台线程里调用 send(),网络长时间中断时
115
+ 缓冲区会持续积压,60s 的阻塞会让日志队列在后台线程里严重堆积、内存上涨。
116
+ 日志是旁路、不可拖垮主进程——宁可丢日志也要尽快返回,因此收紧到 5s。
117
+ (request_timeout_ms=30000 保持不变:那是单次网络请求的超时,与 send() 阻塞
118
+ 上限是两回事。)
119
+ bootstrap_timeout_ms=10000:
120
+ api_version 默认为 None,__init__ 会 eager bootstrap 拉取版本信息,
121
+ broker 不可达时默认 30s 才超时。重建/惰性补建路径下 30s 太长,收到 10s。
91
122
  """
92
123
  return KafkaProducer(
93
124
  bootstrap_servers=self._bootstrap_servers,
94
125
  value_serializer=lambda v: json.dumps(
95
126
  v, ensure_ascii=False).encode('utf-8'),
96
- # 保持原有的优化配置
97
- max_block_ms=60000,
127
+ # 日志旁路不可拖垮主进程:网络断 + 缓冲满时,send() 最长阻塞 5s 后即放弃
128
+ max_block_ms=5000,
98
129
  retries=5,
99
130
  request_timeout_ms=30000,
131
+ # 重建/惰性补建时 eager bootstrap 最长 10s,避免长断网下补建卡 30s
132
+ bootstrap_timeout_ms=10000,
100
133
  compression_type='gzip',
101
134
  batch_size=16384,
102
135
  linger_ms=5,
@@ -114,6 +147,10 @@ class KafkaSink:
114
147
 
115
148
  # producer 已关闭则跳过
116
149
  if self._producer is None:
150
+ # 重建进行中(_rebuild_producer 正在关闭旧的/构造新的):直接跳过本条,
151
+ # 避免和重建线程同时 _build_producer 造成两个 producer 泄漏。
152
+ if self._rebuild_in_progress:
153
+ return
117
154
  # 全局关闭(_enabled=False 时未建 producer):仅放行 ERROR 与
118
155
  # 单条强制日志(enabled=True),并为它们惰性补建一次 producer。
119
156
  record = message.record
@@ -211,13 +248,89 @@ class KafkaSink:
211
248
  "uid": int(Snowflake.id)
212
249
  }
213
250
 
214
- # 8. 发送(添加回调监控发送失败)
251
+ # 8. 发送(添加回调监控发送失败 + 驱动自愈)
215
252
  future = self._producer.send("shengye-json-log", log_entry)
216
- future.add_errback(lambda e: print(f"Kafka send failed: {e}"))
253
+ future.add_errback(self._on_send_failed)
254
+ # send() 本身成功入队,记一次“健康”以清零连续失败计数
255
+ self._on_send_succeeded()
256
+
257
+ except Exception:
258
+ pass
259
+
260
+ def _on_send_succeeded(self):
261
+ """send() 成功把消息放进缓冲区:清零连续失败计数(注意,这只是入队成功,
262
+ 不代表 broker 已 ack;但用于判断 producer 是否还能接收新消息已足够)。"""
263
+ if self._consecutive_failures:
264
+ with self._rebuild_lock:
265
+ self._consecutive_failures = 0
266
+
267
+ def _on_send_failed(self, exc):
268
+ """future 投递失败回调(在 kafka-python 的 IO/sender 线程触发,与 loguru
269
+ 后台线程不是同一个线程,故所有对计数器的读写都在 _rebuild_lock 内)。
217
270
 
271
+ 累计连续失败;达到阈值且过了冷却期就重建 producer。重建本身可能阻塞
272
+ (_build_producer 的 bootstrap),放在 kafka-python 线程里执行会拖慢它的
273
+ 网络收发,因此用一个 try 快速尝试并吞掉所有异常——日志旁路,绝不抛。"""
274
+ try:
275
+ rebuild = False
276
+ with self._rebuild_lock:
277
+ self._consecutive_failures += 1
278
+ if self._consecutive_failures >= self._rebuild_failure_threshold:
279
+ now = time.monotonic()
280
+ if now - self._last_rebuild_ts >= self._rebuild_cooldown_sec:
281
+ rebuild = True
282
+ self._last_rebuild_ts = now
283
+ self._consecutive_failures = 0
284
+ if rebuild:
285
+ self._rebuild_producer()
286
+ # 失败仍打到 stdout,便于运维感知(与原行为一致)
287
+ print(f"Kafka send failed: {exc}")
218
288
  except Exception:
289
+ # 回调里绝不能抛,否则会污染 kafka-python 的 future 处理
219
290
  pass
220
291
 
292
+ def _rebuild_producer(self):
293
+ """关闭旧 producer 并重建一个全新的。
294
+
295
+ 线程模型:本方法由 errback 在 kafka-python 的 sender/IO 线程触发调用,
296
+ 而 write()(含惰性建 producer)在 loguru 后台线程。两者会同时碰 producer
297
+ 引用,故全程用 _rebuild_lock 保护,并置 _rebuild_in_progress 让 write()
298
+ 在此期间跳过惰性建 producer,避免重复构造。
299
+
300
+ 关键:close() 内部会 join sender 线程(sender.join(timeout)),而本方法
301
+ 正是从 sender 线程的 errback 链调进来的——若在锁内直接 close 旧 producer
302
+ 会触发“在自己线程里 join 自己”的死锁。所以 flush/close 必须放在锁外,
303
+ 且把 _rebuild_in_progress 始终维持到新 producer 就位,避免 join 期间又有
304
+ write() 介入。绝不在锁内做 close 这种长阻塞。"""
305
+ with self._rebuild_lock:
306
+ if self._rebuild_in_progress:
307
+ return # 已有重建在进行,不重入
308
+ self._rebuild_in_progress = True
309
+ old = self._producer_obj
310
+ # 摘引用:write() 见到 _producer is None 且 _rebuild_in_progress 会跳过
311
+ self._producer = None
312
+ self._producer_obj = None
313
+ # flush/close 放锁外:close() 会 join sender 线程,绝不能在 sender 线程里持锁做
314
+ if old is not None:
315
+ try:
316
+ old.flush(timeout=5)
317
+ old.close(timeout=10)
318
+ except Exception:
319
+ pass
320
+ # 重建新 producer(bootstrap 不可达会抛,吞掉,下次失败计数还会再触发)
321
+ try:
322
+ new_prod = self._build_producer()
323
+ with self._rebuild_lock:
324
+ self._producer_obj = new_prod
325
+ self._producer = new_prod
326
+ self._rebuild_in_progress = False
327
+ print("[KafkaSink] producer 已自愈重建(连续投递失败达到阈值)")
328
+ except Exception as e:
329
+ # 重建失败也要清标志,否则后续永远重建不了
330
+ with self._rebuild_lock:
331
+ self._rebuild_in_progress = False
332
+ print(f"[KafkaSink] producer 重建失败: {e}")
333
+
221
334
  def flush(self):
222
335
  if self._producer:
223
336
  try:
@@ -242,6 +355,10 @@ class KafkaSink:
242
355
  pass
243
356
  finally:
244
357
  self._producer_obj = None
358
+ # 重置自愈计数器,避免 reload/重建后的 sink 复用了旧状态
359
+ with self._rebuild_lock:
360
+ self._consecutive_failures = 0
361
+ self._last_rebuild_ts = 0.0
245
362
 
246
363
 
247
364
  class _InterceptHandler(logging.Handler):
@@ -689,18 +689,21 @@ def setup_sandbox_handler(app: FastAPI, config: dict = None):
689
689
  SYLogger.warning(f"[Sandbox Server] 文件已存在: {file_path}")
690
690
  return WriteResponse(error=f"Error: File already exists at {req.file_path}. Use edit_file to modify existing files.", path=None)
691
691
 
692
- content = base64.b64decode(req.content).decode('utf-8')
692
+ # 二进制安全写入:base64 解码后直接写 bytes,不再 decode('utf-8')
693
+ # deepagents 0.6.12 的媒体 offload(图片/音频等)可能走 write 路径,
694
+ # 若用 utf-8 解码二进制内容会抛 UnicodeDecodeError。与 /upload 对齐。
695
+ content = base64.b64decode(req.content)
693
696
 
694
697
  parent = os.path.dirname(file_path)
695
698
  if parent:
696
699
  os.makedirs(parent, exist_ok=True)
697
700
 
698
- with open(file_path, 'w') as f:
701
+ with open(file_path, 'wb') as f:
699
702
  f.write(content)
700
703
 
701
704
  action = "覆盖" if (os.path.exists(file_path) and overwrite) else "新建"
702
705
  SYLogger.info(
703
- f"[Sandbox Server] 写入成功({action}): {file_path} ({len(content)} 字符)")
706
+ f"[Sandbox Server] 写入成功({action}): {file_path} ({len(content)} 字节)")
704
707
  virtual_path = _to_virtual_path(file_path, workspace)
705
708
  return WriteResponse(error=None, path=virtual_path)
706
709
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.7a6
3
+ Version: 0.2.7a8
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -9,18 +9,18 @@ Requires-Dist: aiohttp==3.14.1
9
9
  Requires-Dist: aiomysql==0.3.2
10
10
  Requires-Dist: anyio==4.14.1
11
11
  Requires-Dist: decorator==5.3.1
12
- Requires-Dist: deepagents==0.6.11
12
+ Requires-Dist: deepagents==0.6.12
13
13
  Requires-Dist: elasticsearch==9.4.1
14
- Requires-Dist: fastapi==0.138.0
14
+ Requires-Dist: fastapi==0.138.1
15
15
  Requires-Dist: jinja2==3.1.6
16
- Requires-Dist: kafka-python==3.0.2
16
+ Requires-Dist: kafka-python==3.0.6
17
17
  Requires-Dist: langchain==1.3.11
18
18
  Requires-Dist: langchain-core==1.4.8
19
19
  Requires-Dist: langchain-openai==1.3.3
20
- Requires-Dist: langfuse==4.9.1
20
+ Requires-Dist: langfuse==4.12.0
21
21
  Requires-Dist: langgraph==1.2.6
22
22
  Requires-Dist: langgraph-checkpoint-postgres==3.1.0
23
- Requires-Dist: langgraph-checkpoint-redis==0.4.1
23
+ Requires-Dist: langgraph-checkpoint-redis==0.5.0
24
24
  Requires-Dist: ldap3==2.9.1
25
25
  Requires-Dist: loguru==0.7.3
26
26
  Requires-Dist: mysql-connector-python==9.7.0
@@ -136,9 +136,9 @@ sycommon/services.py,sha256=CwC_gESafo05Qn41KLQgtulx6PfX7s2FkxlBI-hXve4,24861
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=ZcqQ8aVo6PYx3YWzV8aEV9HRtuXuoVUyXWGwXUJ_VTw,67278
140
- sycommon/agent/multi_agent_team.py,sha256=mskS-FJbAB_n5kghjHhiWxpA3L9cD1lObeW_wjrLI6s,27079
141
- sycommon/agent/summarization_utils.py,sha256=zAnNtqzZ6mKmLVpLQLgcnbdV0D5ohxpAjPkg9Uz2V7s,17934
139
+ sycommon/agent/deep_agent.py,sha256=ztovDHzIXfqMFmznBgABy4xAA50xttEtFzeo3lR54wg,70290
140
+ sycommon/agent/multi_agent_team.py,sha256=ariJvJKTLMy7BAxig_eMkXCrvELKmS3sUD978pMZDRs,28692
141
+ sycommon/agent/summarization_utils.py,sha256=bqZRFqSekNGlv2mYQPaT_s2QNA5VDTKddi2Va_SkHWc,18376
142
142
  sycommon/agent/acp/__init__.py,sha256=vQGMQYAA5-ZaejYmDZ4r4Fklqs17qH3cDXQ5pZlwj-0,1844
143
143
  sycommon/agent/acp/client.py,sha256=s-K626b9ierwyEq61gejrW0xOysvrY2PIpYFsFqSHjc,27108
144
144
  sycommon/agent/acp/fetch.py,sha256=a6tL13eeGT1KgExgNbdYpzQ54xBqLeTGNTIQmxr3eQ4,6796
@@ -148,13 +148,14 @@ sycommon/agent/mcp/__init__.py,sha256=iKrdDhIrFsNIkqG_kgcwNe-nOiM6uVfolKv44LfQ-F
148
148
  sycommon/agent/mcp/models.py,sha256=zda4ho-UTYOJ5oPAZPZ-vxhfDRLghUCsMroHnA2A2QQ,1490
149
149
  sycommon/agent/mcp/tool_loader.py,sha256=OQa-lE7RL2s3Lqh-kNOKaUrb130hvorB_HjyFBqZBZs,10960
150
150
  sycommon/agent/middleware/sitecustomize.py,sha256=Bt7XFnWV_LJo89l858AGp5VUkXwefpLGmjkCtLe-CMw,4800
151
- sycommon/agent/middleware/skill_api_whitelist.py,sha256=V8QjPwtj0msCxlU10PaXyaWWFnFACjNZg_BFByo_kO4,11263
151
+ sycommon/agent/middleware/skill_api_whitelist.py,sha256=e15aJekXegPp64owSCEdUE5y0SmwCQVQCF4jnhReZNU,10958
152
152
  sycommon/agent/middleware/skill_wl_check.py,sha256=rCJ9F6aWPh8tVQBIPmcq2lKDsfwiJQduwSUku_8kXfs,3952
153
+ sycommon/agent/middleware/skill_write_guard.py,sha256=N7EsHMc87eo5QFLjTxbIcn0kurM1LD3vlfeHSR3jVg4,2557
153
154
  sycommon/agent/sandbox/__init__.py,sha256=jR7LlkD4J4Y6QYyRXQClkwmqDBCCPmycV_hQV9p9YHw,4621
154
155
  sycommon/agent/sandbox/consistent_hash.py,sha256=8Jgk-W4NAD2-u5_vKRVlPmql_e0Vy4aNORiXOuzvIrs,7277
155
156
  sycommon/agent/sandbox/file_ops.py,sha256=7A-T5dV2o5OzPT2kaNrlOTAqmyQ8HGiHV7ifqqMivZA,32988
156
- sycommon/agent/sandbox/http_sandbox_backend.py,sha256=0uQD4juPqUGJ_lQf0aTCgBn81GtansiuaCwGgsLZ7zc,68230
157
- sycommon/agent/sandbox/minio_sync.py,sha256=vGpc2rwfG86q8D_1eAxg_0X1n_6IfYxwQF3waQPgopY,22413
157
+ sycommon/agent/sandbox/http_sandbox_backend.py,sha256=59FvNhU9WdevXx_X14uiN_REWnEtQk01OXT0UQlgoTk,68303
158
+ sycommon/agent/sandbox/minio_sync.py,sha256=HRtw3SwOlgy0G-AsXhWr_1Ob7s-n4Z-9bjpFRfKecTA,23438
158
159
  sycommon/agent/sandbox/sandbox_pool.py,sha256=eMn8sLakCWf90l6ni2-333QM8oBdX1CflV-WzneFp_k,9133
159
160
  sycommon/agent/sandbox/sandbox_recovery.py,sha256=X-eDODx1tmGMh_iTngV6e1ppfDBHpTdkPreJusN5MHY,7358
160
161
  sycommon/agent/sandbox/session.py,sha256=TjzC3yFC-VaJ75UwCyL26QX4PRTGNNfQae1FKFuOsYI,2365
@@ -215,7 +216,7 @@ sycommon/llm/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4,sha256=Ijkh
215
216
  sycommon/llm/tiktoken_cache/fb374d419588a4632f3f557e76b4b70aebbca790,sha256=RGqVOMtsNI41FhINfAiwn1fDZJXirP_-WaW_iwz7Gi0,3613922
216
217
  sycommon/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
218
  sycommon/logging/async_sql_logger.py,sha256=TIUAqK8_F9H7Graw00hNjEI3DJ1ZRo5fRUcnqaMZlbU,2187
218
- sycommon/logging/kafka_log.py,sha256=H_nnSQtCXalxUyKzpjpa_6iTp1brGRRhyQhXH5qEjd0,20656
219
+ sycommon/logging/kafka_log.py,sha256=0DM3OqaBhe1pERpKCWEgCWRJJGTVeYajz8dsSIYPGms,28096
219
220
  sycommon/logging/log_masker.py,sha256=o4yE1rZ8f8a3xwTLV7TZvAt9nPB2IjacKCxoJspbhBM,12797
220
221
  sycommon/logging/logger_levels.py,sha256=xJBzPV2H7GpEQggOIjyckEsA4yf6s4NpI9ckgO3xWKA,1154
221
222
  sycommon/logging/logger_wrapper.py,sha256=TtzmGw_K5NjBGBq9Gjqtnh834izAv8TQwsOeL2fIbAI,481
@@ -231,7 +232,7 @@ sycommon/middleware/exception.py,sha256=UAy0tKijI_2JoKjwT3h62aL-tybftP3IETvcr26N
231
232
  sycommon/middleware/middleware.py,sha256=1qA_0rT7pv1xTfuvA0kdYkxVOHYT6roSSNO7Cmf2wYg,1778
232
233
  sycommon/middleware/monitor_memory.py,sha256=pYRK-wRuDd6enSg9Pf8tQxPdYQS6S0AyjyXeKFRLKEs,628
233
234
  sycommon/middleware/mq.py,sha256=9X6KKtadFjBXKS5L3kEKujYio9wwGfWgXwWOAHO-HDg,254
234
- sycommon/middleware/sandbox.py,sha256=_BjRqJgJDula_Vyg-ooifrqG9L3CaS7_KPLzvyW0JrY,49598
235
+ sycommon/middleware/sandbox.py,sha256=6nt62XLE0Ow-rs-0FZNLUgwgfgkCOGwiIKKDtTjU6D8,49876
235
236
  sycommon/middleware/timeout.py,sha256=KlxOPa8xl2dg6yuRi_EzkVJG8bX4stb5ueYxctzzGM8,1433
236
237
  sycommon/middleware/token_tracking.py,sha256=rEbgV1bgWMdzAERx4aq5XAvOIT6jTY_tK1P0xHJnL3o,6609
237
238
  sycommon/middleware/tool_result_truncation.py,sha256=SREo6tb_RDHxqiQdO0N82pio-Cpo0YSrHNa1sASUafM,12429
@@ -305,8 +306,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
305
306
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
306
307
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
307
308
  sycommon/xxljob/xxljob_service.py,sha256=1yifwIBNGsCIxLnQjHKiBlbsigc_zvPH-dMTZcNxe-Q,7649
308
- sycommon_python_lib-0.2.7a6.dist-info/METADATA,sha256=hU2LQjrN0I2oL5pC9AnipDDJG2khY7jdB1TW98tN3Es,7960
309
- sycommon_python_lib-0.2.7a6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
310
- sycommon_python_lib-0.2.7a6.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
311
- sycommon_python_lib-0.2.7a6.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
312
- sycommon_python_lib-0.2.7a6.dist-info/RECORD,,
309
+ sycommon_python_lib-0.2.7a8.dist-info/METADATA,sha256=IIlGEa1pB6mxNZgON7ofCiMBvWXJGwIkGxrDpqTOgkw,7961
310
+ sycommon_python_lib-0.2.7a8.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
311
+ sycommon_python_lib-0.2.7a8.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
312
+ sycommon_python_lib-0.2.7a8.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
313
+ sycommon_python_lib-0.2.7a8.dist-info/RECORD,,