From f80a00e2119f7077e019fa4b6e76dfc1d5929e72 Mon Sep 17 00:00:00 2001 From: xuhd Date: Mon, 18 May 2026 15:25:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(skills):=20curator=20=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E5=8A=A0=20pinned=20=E8=AE=A1=E6=95=B0=20+=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E8=BE=93=E5=87=BA=E6=8A=80=E8=83=BD=E5=90=8D?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_curator_status 的 agent_created_skills 增加 pinned 计数,与 active/stale/archived 正交;前端技能进化面板同步展示「被钉住」一项 (curator/types.ts、skill-evolution-panel.tsx、zh-CN/en-US/types i18n)。 - run_curator_review 每次运行时把涉及的技能名输出到日志:新增 _log_scanned_skills(扫描到的技能名)与 _candidate_skill_names (候选技能名),logger 与 curator_log 双写。 新增 test_curator_status_counts.py、test_curator_skill_logging.py。 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/skill-evolution-panel.tsx | 3 + frontend-web/src/core/curator/types.ts | 2 + frontend-web/src/core/i18n/locales/en-US.ts | 1 + frontend-web/src/core/i18n/locales/types.ts | 1 + frontend-web/src/core/i18n/locales/zh-CN.ts | 1 + .../harness/deerflow/skills/curator.py | 58 +++++++++++++++++- .../tests/test_curator_skill_logging.py | 56 ++++++++++++++++++ .../tests/test_curator_status_counts.py | 59 +++++++++++++++++++ 8 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 offline-backend-20260512/backend/tests/test_curator_skill_logging.py create mode 100644 offline-backend-20260512/backend/tests/test_curator_status_counts.py diff --git a/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx b/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx index b9549ed6..10fa0a9e 100644 --- a/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx +++ b/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx @@ -177,6 +177,9 @@ function StatusCard() { {tt.archived}: {status.agent_created_skills.archived} + + {tt.pinned}: {status.agent_created_skills.pinned} + diff --git a/frontend-web/src/core/curator/types.ts b/frontend-web/src/core/curator/types.ts index b8a2f9d8..7f350831 100644 --- a/frontend-web/src/core/curator/types.ts +++ b/frontend-web/src/core/curator/types.ts @@ -47,6 +47,8 @@ export interface CuratorStatus { active: number; stale: number; archived: number; + /** 被 pin 的数量;与 active/stale/archived 正交。 */ + pinned: number; }; /** 上次运行 LLM 自述的建议(它"说"要做什么)。 */ last_consolidation_suggestions: CuratorSuggestions; diff --git a/frontend-web/src/core/i18n/locales/en-US.ts b/frontend-web/src/core/i18n/locales/en-US.ts index ff22341b..202be30d 100644 --- a/frontend-web/src/core/i18n/locales/en-US.ts +++ b/frontend-web/src/core/i18n/locales/en-US.ts @@ -546,6 +546,7 @@ export const enUS: Translations = { stale: "Stale", archived: "Archived", total: "Total", + pinned: "Pinned", runNow: "Run now", runStarted: "Background run started", runBlockedEvolutionDisabled: diff --git a/frontend-web/src/core/i18n/locales/types.ts b/frontend-web/src/core/i18n/locales/types.ts index 74fa167c..3ac92d20 100644 --- a/frontend-web/src/core/i18n/locales/types.ts +++ b/frontend-web/src/core/i18n/locales/types.ts @@ -474,6 +474,7 @@ export interface Translations { stale: string; archived: string; total: string; + pinned: string; runNow: string; runStarted: string; runBlockedEvolutionDisabled: string; diff --git a/frontend-web/src/core/i18n/locales/zh-CN.ts b/frontend-web/src/core/i18n/locales/zh-CN.ts index 867c060d..a32bbe05 100644 --- a/frontend-web/src/core/i18n/locales/zh-CN.ts +++ b/frontend-web/src/core/i18n/locales/zh-CN.ts @@ -522,6 +522,7 @@ export const zhCN: Translations = { stale: "陈旧", archived: "已归档", total: "合计", + pinned: "被钉住", runNow: "立即执行", runStarted: "已触发后台执行", runBlockedEvolutionDisabled: "自我进化未开启 —— 请先在下方打开开关。", diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/skills/curator.py b/offline-backend-20260512/backend/packages/harness/deerflow/skills/curator.py index ce70133f..ddb71b2f 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/skills/curator.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/skills/curator.py @@ -556,6 +556,41 @@ def _render_candidate_list(user_id: str) -> str: return "\n".join(lines) +def _candidate_skill_names(candidate_list: str) -> list[str]: + """从 :func:`_render_candidate_list` 的文本里抽出技能名。 + + 候选行形如 ``- {name} [state=..., ...]``,技能名是 ``- `` 之后的第一个 token。 + """ + return [ + line[2:].split(" ", 1)[0] + for line in candidate_list.splitlines() + if line.startswith("- ") + ] + + +def _log_scanned_skills(user_id: str) -> list[str]: + """记录并返回本次策展扫描到的技能名(Agent 创建、属于该用户)。 + + 每次技能进化运行时调用 —— 把涉及的技能名打到日志,便于排查"哪些技能 + 被策展程序处理了"。返回排序后的技能名列表。 + """ + from deerflow.skills import curator_log + from deerflow.skills.usage import agent_created_entries + + owned = _list_user_owned_skill_names(user_id) + scanned = agent_created_entries() + if owned is not None: + scanned = {n: e for n, e in scanned.items() if n in owned} + names = sorted(scanned) + logger.info("Curator[%s]: 本次扫描到 %d 个技能 — %s", user_id, len(names), names) + curator_log.record( + "info", + f"本次扫描到 {len(names)} 个 Agent 创建的技能:{', '.join(names) or '(无)'}", + user_id=user_id, + ) + return names + + _CURATOR_REVIEW_PROMPT = ( "You are running as DeerFlow's background skill CURATOR. This is an " "UMBRELLA-BUILDING consolidation pass, not a passive audit and not a " @@ -959,6 +994,9 @@ def run_curator_review(user_id: str) -> dict: logger.info("Curator[%s]: starting run at %s", user_id, now.isoformat()) curator_log.record("info", f"本次策展开始,启动时间 {now.isoformat()}", user_id=user_id) + # 输出本次技能进化涉及的技能名,便于排查。 + _log_scanned_skills(user_id) + transition_counters = apply_automatic_transitions(user_id, now) logger.info("Curator[%s]: transitions=%s", user_id, transition_counters) curator_log.record( @@ -972,9 +1010,19 @@ def run_curator_review(user_id: str) -> dict: ) candidate_list = _render_candidate_list(user_id) - candidate_line_count = 0 if not candidate_list.strip() else len([line for line in candidate_list.splitlines() if line.startswith("- ")]) + candidate_names = _candidate_skill_names(candidate_list) + candidate_line_count = len(candidate_names) + logger.info( + "Curator[%s]: %d 个候选技能进入 LLM 评审 — %s", + user_id, + candidate_line_count, + candidate_names, + ) curator_log.record( - "info", f"进入 LLM 评审的候选技能:共 {candidate_line_count} 个", user_id=user_id + "info", + f"进入 LLM 评审的候选技能:共 {candidate_line_count} 个" + + (f" —— {', '.join(candidate_names)}" if candidate_names else ""), + user_id=user_id, ) if candidate_line_count == 0: @@ -1168,9 +1216,13 @@ def get_curator_status(user_id: str) -> dict: if owned is not None: entries = {name: entry for name, entry in entries.items() if name in owned} counts: dict[str, int] = {STATE_ACTIVE: 0, STATE_STALE: 0, STATE_ARCHIVED: 0} + pinned_count = 0 for entry in entries.values(): s = entry.get("state", STATE_ACTIVE) counts[s] = counts.get(s, 0) + 1 + # pinned 与生命周期状态正交 —— 单独计数。 + if entry.get("pinned"): + pinned_count += 1 cfg = _get_curator_config(user_id) return { @@ -1184,6 +1236,8 @@ def get_curator_status(user_id: str) -> dict: "active": counts[STATE_ACTIVE], "stale": counts[STATE_STALE], "archived": counts[STATE_ARCHIVED], + # 被 pin 的数量;与 active/stale/archived 正交,不计入它们的和。 + "pinned": pinned_count, }, # 上次运行的两份记录:LLM 自述的建议 + 实际发生的工具调用,供前端对照展示。 "last_consolidation_suggestions": state.get("last_consolidation_suggestions") diff --git a/offline-backend-20260512/backend/tests/test_curator_skill_logging.py b/offline-backend-20260512/backend/tests/test_curator_skill_logging.py new file mode 100644 index 00000000..970413ca --- /dev/null +++ b/offline-backend-20260512/backend/tests/test_curator_skill_logging.py @@ -0,0 +1,56 @@ +"""技能进化运行时把涉及的技能名输出到日志。""" + +from __future__ import annotations + +import logging + +from deerflow.skills import curator + +_CURATOR_LOGGER = "deerflow.skills.curator" + + +class TestCandidateSkillNames: + def test_extracts_names_from_candidate_lines(self): + text = "- alpha [state=active, use=1]\n- beta-skill [state=stale, use=0]" + assert curator._candidate_skill_names(text) == ["alpha", "beta-skill"] + + def test_ignores_non_candidate_lines(self): + # _render_candidate_list 无候选时返回的提示文本不应被误识别为技能名 + assert curator._candidate_skill_names("No agent-created active skills found.") == [] + + def test_empty(self): + assert curator._candidate_skill_names("") == [] + + +class TestLogScannedSkills: + def _patch_common(self, monkeypatch, entries: dict, owned=None) -> None: + monkeypatch.setattr(curator, "_list_user_owned_skill_names", lambda uid: owned) + monkeypatch.setattr("deerflow.skills.usage.agent_created_entries", lambda: entries) + monkeypatch.setattr("deerflow.skills.curator_log.record", lambda *a, **k: None) + + def test_logs_and_returns_sorted_skill_names(self, monkeypatch, caplog): + self._patch_common( + monkeypatch, + {"zeta": {"source": "agent"}, "alpha": {"source": "agent"}}, + ) + with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER): + names = curator._log_scanned_skills("u1") + assert names == ["alpha", "zeta"] + assert "alpha" in caplog.text + assert "zeta" in caplog.text + + def test_filters_to_owned_skills(self, monkeypatch, caplog): + self._patch_common( + monkeypatch, + {"alpha": {"source": "agent"}, "beta": {"source": "agent"}}, + owned={"alpha"}, + ) + with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER): + names = curator._log_scanned_skills("u1") + assert names == ["alpha"] + + def test_empty_skills(self, monkeypatch, caplog): + self._patch_common(monkeypatch, {}) + with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER): + names = curator._log_scanned_skills("u1") + assert names == [] diff --git a/offline-backend-20260512/backend/tests/test_curator_status_counts.py b/offline-backend-20260512/backend/tests/test_curator_status_counts.py new file mode 100644 index 00000000..02bc388b --- /dev/null +++ b/offline-backend-20260512/backend/tests/test_curator_status_counts.py @@ -0,0 +1,59 @@ +"""get_curator_status 的技能计数 —— 含与生命周期正交的 pinned 计数。""" + +from __future__ import annotations + +from deerflow.config.skill_evolution_config import CuratorConfig +from deerflow.skills import curator + + +def _patch(monkeypatch, entries: dict) -> None: + """注入受控依赖:固定 curator 状态/配置,owned 用 legacy 全量,entries 由测试给定。""" + monkeypatch.setattr(curator, "_read_curator_state", lambda uid: {}) + monkeypatch.setattr(curator, "_list_user_owned_skill_names", lambda uid: None) + monkeypatch.setattr(curator, "_get_curator_config", lambda uid: CuratorConfig(enabled=True)) + monkeypatch.setattr( + "deerflow.skills.usage.agent_created_entries", + lambda: entries, + ) + + +def test_counts_by_state(monkeypatch): + entries = { + "a": {"source": "agent", "state": "active"}, + "b": {"source": "agent", "state": "active"}, + "c": {"source": "agent", "state": "stale"}, + "d": {"source": "agent", "state": "archived"}, + } + _patch(monkeypatch, entries) + counts = curator.get_curator_status("u1")["agent_created_skills"] + assert counts == {"total": 4, "active": 2, "stale": 1, "archived": 1, "pinned": 0} + + +def test_pinned_count_is_orthogonal_to_state(monkeypatch): + # pinned 与 active/stale/archived 正交:一个 active+pinned、一个 archived+pinned + entries = { + "a": {"source": "agent", "state": "active", "pinned": True}, + "b": {"source": "agent", "state": "active"}, + "c": {"source": "agent", "state": "archived", "pinned": True}, + "d": {"source": "agent", "state": "stale"}, + } + _patch(monkeypatch, entries) + counts = curator.get_curator_status("u1")["agent_created_skills"] + assert counts["total"] == 4 + assert counts["active"] == 2 + assert counts["stale"] == 1 + assert counts["archived"] == 1 + # pinned 单独统计,不受状态影响,也不计入上面三者之和 + assert counts["pinned"] == 2 + + +def test_pinned_zero_when_none_pinned(monkeypatch): + _patch(monkeypatch, {"a": {"source": "agent", "state": "active"}}) + counts = curator.get_curator_status("u1")["agent_created_skills"] + assert counts["pinned"] == 0 + + +def test_empty_skills(monkeypatch): + _patch(monkeypatch, {}) + counts = curator.get_curator_status("u1")["agent_created_skills"] + assert counts == {"total": 0, "active": 0, "stale": 0, "archived": 0, "pinned": 0}