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}