diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py b/offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py index c9101044..7d640a0b 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py @@ -153,6 +153,43 @@ def _resolve_skills_path(path: str) -> str: return _join_path_preserving_style(skills_host, relative) +def _custom_skill_name_from_path(path: str) -> str | None: + """从虚拟技能路径里解析出 custom 技能名,用于"被使用"埋点。 + + 只识别 ``{skills_container}/custom/{name}/...`` 形式 —— public 技能不参与 + 策展,给它们埋点只会往 ``.usage.json`` 写无用条目。读技能目录下任意文件 + (SKILL.md 或 references/templates/scripts 支持文件)都算一次该技能的使用。 + 解析不出技能名时返回 None。 + """ + skills_container = _get_skills_container_path() + if not (path == skills_container or path.startswith(f"{skills_container}/")): + return None + relative = path[len(skills_container) :].lstrip("/") + parts = [segment for segment in relative.split("/") if segment] + if len(parts) >= 2 and parts[0] == "custom": + return parts[1] + return None + + +def _record_skill_read(virtual_path: str) -> None: + """技能文件被 ``read_file`` 读取时记一次 use —— 技能"被使用"的真实信号。 + + 技能正文不进 system prompt(prompt 里只有 name/description/location),Agent + 按 Progressive Loading 用 ``read_file`` 加载 SKILL.md 才算真正用到该技能。 + curator 的年龄流转据此判断闲置,没有这个埋点会把每天都在用的技能误判为陈旧。 + best-effort:埋点失败绝不影响 ``read_file`` 本身。 + """ + try: + name = _custom_skill_name_from_path(virtual_path) + if name is None: + return + from deerflow.skills.usage import bump_use + + bump_use(name) + except Exception: + pass + + def _is_acp_workspace_path(path: str) -> bool: """Check if a path is under the ACP workspace virtual path.""" return path == _ACP_WORKSPACE_VIRTUAL_PATH or path.startswith(f"{_ACP_WORKSPACE_VIRTUAL_PATH}/") @@ -1467,6 +1504,9 @@ def read_file_tool( path = _resolve_and_validate_user_data_path(path, thread_data) # Custom mount paths are resolved by LocalSandbox._resolve_path() content = sandbox.read_file(path) + # 读成功即记一次技能使用 —— requested_path 仍是虚拟路径(本地/容器沙箱都一样), + # 非技能路径会被 _record_skill_read 自行忽略。 + _record_skill_read(requested_path) if not content: return "(empty)" if start_line is not None and end_line is not None: diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/tools/skill_manage_tool.py b/offline-backend-20260512/backend/packages/harness/deerflow/tools/skill_manage_tool.py index 7fb8fde3..13d9526a 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/tools/skill_manage_tool.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/tools/skill_manage_tool.py @@ -66,6 +66,21 @@ async def _to_thread(func, /, *args, **kwargs): return await asyncio.to_thread(func, *args, **kwargs) +async def _bump_patch_best_effort(name: str) -> None: + """记一次技能维护活动(patch / edit)。 + + 刷新 ``.usage.json`` 的 ``patch_count`` 与 ``last_patched_at``,让 curator 的 + 年龄流转把"Agent 正在主动维护的技能"算作活跃,而不是误判为闲置后归档。 + best-effort:埋点失败只记 DEBUG 日志,绝不影响技能写入主流程。 + """ + try: + from deerflow.skills.usage import bump_patch + + await _to_thread(bump_patch, name) + except Exception: + logger.debug("Failed to bump patch count for %r", name, exc_info=True) + + async def _ensure_skill_db_registered(store: SkillStore, name: str, user_id: str) -> None: """Idempotently record an agent-created skill's ownership in the DB. @@ -229,6 +244,7 @@ async def _skill_manage_impl( name, _history_record(action="edit", file_path=SKILL_MD_FILE, prev_content=prev_content, new_content=content, thread_id=thread_id, scanner=scan), ) + await _bump_patch_best_effort(name) await refresh_skills_system_prompt_cache_async() return f"Updated custom skill '{name}'." @@ -253,6 +269,7 @@ async def _skill_manage_impl( name, _history_record(action="patch", file_path=SKILL_MD_FILE, prev_content=prev_content, new_content=new_content, thread_id=thread_id, scanner=scan), ) + await _bump_patch_best_effort(name) await refresh_skills_system_prompt_cache_async() return f"Patched custom skill '{name}' ({replacement_count} replacement(s) applied, {occurrences} match(es) found)."