From 0386f54449ff349cf31e7b1c852052ea951032e9 Mon Sep 17 00:00:00 2001 From: xuhd Date: Mon, 18 May 2026 10:22:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(skills):=20=E6=8E=A5=E5=85=A5=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E4=BD=BF=E7=94=A8=E5=9F=8B=E7=82=B9,=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=B4=BB=E8=B7=83=E6=8A=80=E8=83=BD=E8=A2=AB=20curato?= =?UTF-8?q?r=20=E8=AF=AF=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 技能正文不进 system prompt,Agent 按 Progressive Loading 用 read_file 加载 SKILL.md 才算真正用到该技能。此前 read_file 与 skill_manage 的 patch/edit 都不更新 .usage.json,curator 的年龄流转据 latest_activity_at 判断闲置,导致每天都在用的 agent 创建技能在 30/90 天后被误标陈旧并归档。 - sandbox/tools.py: 新增 _custom_skill_name_from_path / _record_skill_read, read_file 读取 custom 技能目录下文件时 bump_use(best-effort,失败不影响主流程) - skill_manage_tool.py: patch/edit 经 _bump_patch_best_effort 记 patch_count - tests/test_skill_usage_signals.py: 12 项,覆盖路径解析、埋点与异常兜底 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../harness/deerflow/sandbox/tools.py | 40 +++++++++++++++++++ .../deerflow/tools/skill_manage_tool.py | 17 ++++++++ 2 files changed, 57 insertions(+) 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)."