sycommon-python-lib 0.2.5a40__py3-none-any.whl → 0.2.5a42__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.
@@ -1221,31 +1221,74 @@ async def create_deep_agent(
1221
1221
 
1222
1222
 
1223
1223
  def _parse_skill_version(content: str) -> str:
1224
- """从 SKILL.md 内容中解析 version 字段,无 version 返回空字符串"""
1225
- match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
1226
- if not match:
1227
- return ""
1228
- try:
1229
- data = yaml.safe_load(match.group(1))
1230
- if isinstance(data, dict):
1231
- return str(data.get("version", ""))
1232
- except Exception:
1233
- pass
1224
+ """从 SKILL.md 内容中解析 version 字段,无 version 返回空字符串
1225
+
1226
+ 使用逐行解析而非 yaml.safe_load,避免 description 中含冒号导致 YAML 解析失败。
1227
+ """
1228
+ in_frontmatter = False
1229
+ for line in content.split("\n"):
1230
+ stripped = line.strip()
1231
+ if stripped == "---":
1232
+ if in_frontmatter:
1233
+ break # 结束 frontmatter
1234
+ in_frontmatter = True
1235
+ continue
1236
+ if in_frontmatter and stripped.startswith("version:"):
1237
+ return stripped.split(":", 1)[1].strip()
1234
1238
  return ""
1235
1239
 
1236
1240
 
1241
+ async def _sync_skill_to_minio(
1242
+ minio_service,
1243
+ local_skill_dir: str,
1244
+ skill_name: str,
1245
+ ) -> int:
1246
+ """将单个技能目录上传到 MinIO(skills/{skill_name}/...)
1247
+
1248
+ Returns:
1249
+ 成功上传的文件数量
1250
+ """
1251
+ count = 0
1252
+ for root, _dirs, files in os.walk(local_skill_dir):
1253
+ for fname in files:
1254
+ fpath = os.path.join(root, fname)
1255
+ rel = os.path.relpath(fpath, local_skill_dir)
1256
+ # 统一使用 / 作为分隔符
1257
+ object_key = f"skills/{skill_name}/{rel.replace(os.sep, '/')}"
1258
+ try:
1259
+ content = await asyncio.to_thread(
1260
+ lambda p: open(p, "rb").read(), fpath)
1261
+ await minio_service.aupload_bytes(object_key, content)
1262
+ count += 1
1263
+ except Exception as e:
1264
+ SYLogger.warning(
1265
+ f"[MinIO] 技能文件上传失败: {object_key}, {e}")
1266
+ SYLogger.info(f"[MinIO] 技能 {skill_name} 同步完成: {count} 个文件")
1267
+ return count
1268
+
1269
+
1237
1270
  async def _sync_skills_to_sandbox(
1238
1271
  skills_dir: str,
1239
1272
  backend: HTTPSandboxBackend,
1240
1273
  ) -> None:
1241
- """同步 skills SKILL.md 到沙箱(按版本检查,版本不一致则覆盖)
1274
+ """同步技能目录到沙箱和 MinIO(按版本检查,版本不一致则全量覆盖)
1242
1275
 
1243
- 只同步 SKILL.md 文件,不同步 scripts/ 等大目录。
1244
- scripts/ 在 agent 实际调用时由 HTTPSandboxBackend._lazy_sync_skill_from_path 按需同步。
1276
+ 遍历本地 skills_dir 下所有技能子目录,比较本地与沙箱的 SKILL.md version:
1277
+ - 版本一致:跳过
1278
+ - 版本不一致或沙箱无版本:删除旧目录,全量上传到沙箱和 MinIO
1279
+ 同步完成后将技能名加入 backend._synced_skills,跳过后续懒同步。
1245
1280
  """
1246
1281
  if not os.path.isdir(skills_dir):
1247
1282
  return
1248
1283
 
1284
+ try:
1285
+ from sycommon.agent.sandbox.minio_sync import MinioSyncService
1286
+ minio_available = MinioSyncService.is_available()
1287
+ minio_service = MinioSyncService() if minio_available else None
1288
+ except Exception:
1289
+ minio_available = False
1290
+ minio_service = None
1291
+
1249
1292
  try:
1250
1293
  # 获取沙箱 /skills 下已有的目录列表
1251
1294
  existing_dirs = set()
@@ -1255,8 +1298,9 @@ async def _sync_skills_to_sandbox(
1255
1298
  if child.is_dir:
1256
1299
  existing_dirs.add(child.name)
1257
1300
 
1258
- # 遍历本地 skills,按版本决定是否同步
1259
- for skill_name in os.listdir(skills_dir):
1301
+ skill_count = 0
1302
+ skip_count = 0
1303
+ for skill_name in sorted(os.listdir(skills_dir)):
1260
1304
  src = os.path.join(skills_dir, skill_name)
1261
1305
  if not os.path.isdir(src):
1262
1306
  continue
@@ -1271,11 +1315,12 @@ async def _sync_skills_to_sandbox(
1271
1315
  except Exception:
1272
1316
  pass
1273
1317
 
1318
+ # 比较版本:一致则跳过
1274
1319
  if skill_name in existing_dirs:
1275
- # 读取沙箱中的版本
1276
1320
  remote_version = ""
1277
1321
  try:
1278
- remote_content = await backend.aread(f"/skills/{skill_name}/SKILL.md")
1322
+ remote_content = await backend.aread(
1323
+ f"/skills/{skill_name}/SKILL.md")
1279
1324
  if remote_content:
1280
1325
  remote_version = _parse_skill_version(remote_content)
1281
1326
  except Exception:
@@ -1284,27 +1329,33 @@ async def _sync_skills_to_sandbox(
1284
1329
  if remote_version and remote_version == local_version:
1285
1330
  SYLogger.debug(
1286
1331
  f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
1332
+ backend._synced_skills.add(skill_name)
1333
+ skip_count += 1
1287
1334
  continue
1288
1335
 
1289
- # 版本不一致或沙箱无版本,删除后重新上传
1336
+ # 版本不一致或沙箱无版本,删除旧目录后全量覆盖
1290
1337
  try:
1291
1338
  await backend.aexecute(f"rm -rf /skills/{skill_name}")
1292
1339
  SYLogger.info(
1293
- f"[DeepAgent] 技能版本不一致,覆盖: {skill_name} ({remote_version or '无版本'} → {local_version})")
1340
+ f"[DeepAgent] 技能版本不一致,覆盖: {skill_name} "
1341
+ f"({remote_version or '无版本'} → {local_version})")
1294
1342
  except Exception as e:
1295
1343
  SYLogger.warning(
1296
1344
  f"[DeepAgent] 删除旧版技能失败: {skill_name}, {e}")
1297
- continue
1298
1345
 
1299
- # 只同步 SKILL.md(不再同步整个目录)
1300
- if os.path.isfile(local_skill_md):
1301
- with open(local_skill_md, "rb") as f:
1302
- content = f.read()
1303
- await backend.aupload_files([
1304
- (f"/skills/{skill_name}/SKILL.md", content)
1305
- ])
1306
- SYLogger.info(
1307
- f"[DeepAgent] 同步技能元数据到沙箱: {skill_name} v{local_version} (仅 SKILL.md)")
1346
+ # 全量上传整个技能目录到沙箱
1347
+ await backend.async_sync_dirs([(src, f"/skills/{skill_name}")])
1348
+ backend._synced_skills.add(skill_name)
1349
+ skill_count += 1
1350
+ SYLogger.info(
1351
+ f"[DeepAgent] 全量同步技能到沙箱: {skill_name} v{local_version}")
1352
+
1353
+ # 同步到 MinIO
1354
+ if minio_service:
1355
+ await _sync_skill_to_minio(minio_service, src, skill_name)
1356
+
1357
+ SYLogger.info(
1358
+ f"[DeepAgent] 技能同步完成: 同步 {skill_count} 个, 跳过 {skip_count} 个")
1308
1359
  except Exception as e:
1309
1360
  SYLogger.warning(f"[DeepAgent] 技能同步失败: {e}")
1310
1361
 
@@ -267,11 +267,45 @@ class WeComLDAPService(metaclass=SingletonMeta):
267
267
  async with session.post(url, json=json_body or {}) as resp:
268
268
  return await resp.json()
269
269
 
270
+ async def _get_bot_access_token(self) -> str:
271
+ """获取用于 open_userid 解码的 access_token
272
+
273
+ 本地开发(IS_DEV=true)时,使用 WeComDecodeUserID 配置的凭据,
274
+ 线上直接使用 WeCom 配置的凭据。
275
+ """
276
+ import os
277
+ from sycommon.config.Config import Config
278
+
279
+ is_dev = os.environ.get("IS_DEV") == "true"
280
+ config = Config().config.get("llm", {})
281
+
282
+ if is_dev:
283
+ wecom_config = config.get("WeComDecodeUserID", {}) or config.get("WeCom", {})
284
+ else:
285
+ wecom_config = config.get("WeCom", {})
286
+
287
+ corpid = wecom_config.get("corpid", "")
288
+ corpsecret = wecom_config.get("corpsecret", "")
289
+ if not corpid or not corpsecret:
290
+ raise ValueError("解码凭据配置不完整,需要配置 llm.WeComDecodeUserID 或 llm.WeCom")
291
+
292
+ url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
293
+ params = {"corpid": corpid, "corpsecret": corpsecret}
294
+ async with aiohttp.ClientSession() as session:
295
+ async with session.get(url, params=params) as resp:
296
+ data = await resp.json()
297
+
298
+ if data.get("errcode") != 0:
299
+ raise RuntimeError(f"获取解码 access_token 失败: {data}")
300
+
301
+ logging.info("[WeComLDAP] 解码 access_token 已刷新")
302
+ return data["access_token"]
303
+
270
304
  async def _decode_open_userid(self, open_userid: str) -> Optional[str]:
271
305
  """将企微密文 open_userid 转换为明文 userid
272
306
 
273
- userid 长度超过 20 位时,大概率是智能机器人的密文 open_userid,
274
- 需要通过 batch/openuserid_to_userid 接口解码。
307
+ 使用机器人应用的 access_token 调用 batch/openuserid_to_userid 接口解码。
308
+ 机器人回调推的 userid 是加密的 open_userid,只有机器人应用的 token 才能转换。
275
309
 
276
310
  Args:
277
311
  open_userid: 密文格式的企微用户ID
@@ -280,10 +314,14 @@ class WeComLDAPService(metaclass=SingletonMeta):
280
314
  解码后的明文 userid,失败返回 None
281
315
  """
282
316
  try:
283
- data = await self._wecom_post(
284
- "/cgi-bin/batch/openuserid_to_userid",
285
- {"open_userid_list": [open_userid]},
286
- )
317
+ token = await self._get_bot_access_token()
318
+ url = "https://qyapi.weixin.qq.com/cgi-bin/batch/openuserid_to_userid"
319
+ async with aiohttp.ClientSession() as session:
320
+ async with session.post(
321
+ f"{url}?access_token={token}",
322
+ json={"open_userid_list": [open_userid]},
323
+ ) as resp:
324
+ data = await resp.json()
287
325
  if data.get("errcode") != 0:
288
326
  SYLogger.warning(
289
327
  f"[WeComLDAP] open_userid 解码失败: {data}"
@@ -334,10 +372,16 @@ class WeComLDAPService(metaclass=SingletonMeta):
334
372
  return decoded if decoded else userid
335
373
 
336
374
  async def _get_wecom_user(self, userid: str) -> Optional[dict]:
337
- """获取单个企微用户详情"""
338
- data = await self._wecom_get(
339
- "/cgi-bin/user/get", {"userid": userid}
340
- )
375
+ """获取单个企微用户详情
376
+
377
+ IS_DEV=true 时使用 WeComDecodeUserID 的 token,
378
+ 线上使用 WeCom 的 token。
379
+ """
380
+ token = await self._get_bot_access_token()
381
+ url = f"https://qyapi.weixin.qq.com/cgi-bin/user/get"
382
+ async with aiohttp.ClientSession() as session:
383
+ async with session.get(url, params={"access_token": token, "userid": userid}) as resp:
384
+ data = await resp.json()
341
385
  if data.get("errcode") != 0:
342
386
  SYLogger.warning(f"[WeComLDAP] 获取企微用户 {userid} 失败: {data}")
343
387
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.5a40
3
+ Version: 0.2.5a42
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -129,7 +129,7 @@ sycommon/services.py,sha256=pUcV0xFrn1hrArD9YwE8m6CMrrWPwNBAqUIWhgsVkzY,24943
129
129
  sycommon/agent/__init__.py,sha256=mxceAeUifQ-DKvWp7ZEJIFlmOCb5wpYHPGQw3rwEN8I,4378
130
130
  sycommon/agent/agent_manager.py,sha256=UhhaekEumT7g4v_Z1UB4jTp13X0n8M8erYaQdkGGWkA,13620
131
131
  sycommon/agent/chat_events.py,sha256=t7qWa6OrIWLfqtd1AnqaVP67QWkn1JxpCBTZYQpoLuM,14226
132
- sycommon/agent/deep_agent.py,sha256=rJXVE-bA9Ypi1VDdT4iEHDuonEM3FsA5qWuIYSt8GtA,63362
132
+ sycommon/agent/deep_agent.py,sha256=KaFYhb6yDcjKPqOS-hUpqzFmmAiuOuCUKMQzkr0W7DU,65273
133
133
  sycommon/agent/multi_agent_team.py,sha256=SRCZHswfflU1w38_YaF0pqh20qa5N5xqcGAVCBoZzmM,26675
134
134
  sycommon/agent/summarization_utils.py,sha256=fiwvKYE_eQc1EtBiPT4UyOZoqQdd4JwEsLRDsJNioLQ,14928
135
135
  sycommon/agent/mcp/__init__.py,sha256=iKrdDhIrFsNIkqG_kgcwNe-nOiM6uVfolKv44LfQ-FQ,636
@@ -147,7 +147,7 @@ sycommon/auth/ldap_service.py,sha256=fOcpVov5LWJkBk62qbTaltks1c4la7JsbD104KfdBOI
147
147
  sycommon/auth/oa_cache.py,sha256=u673y-mK-xo24pSqvfjL68_YFASO2zoxd_iAQ0HetCo,3491
148
148
  sycommon/auth/oa_crypto.py,sha256=xpY1R1Bj3KLENXB0TuThB6Eku1E9PYjcoSpOdgDmCgc,1898
149
149
  sycommon/auth/oa_service.py,sha256=kLepV9zgqpZoaB73DRPpMA5tJJQjoaDtQPdzBcGXeak,6235
150
- sycommon/auth/wecom_ldap_service.py,sha256=CIrGcAr2yD1VTTT3pxzjLCGcH2SxrZrjWM8WkCrx7nk,22829
150
+ sycommon/auth/wecom_ldap_service.py,sha256=VTGw8UQFBCH-lhJZ3N5TRcLy7Qk3Q4uENisXYFhkESk,24817
151
151
  sycommon/config/Config.py,sha256=J5VjolqHmhYTBZwTyfSHv9X5cHMfN6kqY2ryHF4LJPw,6785
152
152
  sycommon/config/DatabaseConfig.py,sha256=ILiUuYT9_xJZE2W-RYuC3JCt_YLKc1sbH13-MHIOPhg,804
153
153
  sycommon/config/ElasticsearchConfig.py,sha256=fO9ZPMgJxSg1-UyDJ90wO6UvYy-jscwPJsSkXgx9qTU,2308
@@ -282,8 +282,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
282
282
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
283
283
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
284
284
  sycommon/xxljob/xxljob_service.py,sha256=JIEJaGXhqrTLcyxlyynSrsHg9bBnDNzX-D4qIWLRPUE,6815
285
- sycommon_python_lib-0.2.5a40.dist-info/METADATA,sha256=oU0fnG1siYsT6N3ZR2mdFPrVtUjLc4FLdG8Dpout6a4,7914
286
- sycommon_python_lib-0.2.5a40.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
287
- sycommon_python_lib-0.2.5a40.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
288
- sycommon_python_lib-0.2.5a40.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
289
- sycommon_python_lib-0.2.5a40.dist-info/RECORD,,
285
+ sycommon_python_lib-0.2.5a42.dist-info/METADATA,sha256=1W1XtiQJptn_RJ5dRXRg8br_cDUEl9FK4lCRxu1PCiA,7914
286
+ sycommon_python_lib-0.2.5a42.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
287
+ sycommon_python_lib-0.2.5a42.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
288
+ sycommon_python_lib-0.2.5a42.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
289
+ sycommon_python_lib-0.2.5a42.dist-info/RECORD,,