@
feat(skills): skill_list 可见性过滤 + skill 只读工具常驻 + 进化面板对照展示 - skill_list:默认 lead agent 改为只暴露未归档、且内置或属于当前用户的技能; 自定义 agent 仍严格按白名单。SkillStore 不可用时降级放行全部自定义技能。 - prompt:注入系统提示词的技能列表剔除已归档(state=archived)技能。 - tools:skill_list/skill_view 只读工具改为常驻提供;skill_manage 写工具仍按 生效配置(含 per-user 覆盖)的 skill_evolution.enabled 门控。 - curator:should_run_now 解析异常时改为不运行;get_curator_status 返回上次 LLM 建议与实际工具调用两份记录,供前端对照。 - 前端:技能进化面板新增建议 vs 实际动作对照展示 + 配套 i18n。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> @
This commit is contained in:
1 parent
d592e1e535
commit
be757b1f2f
9 files changed
+278
-58
No files matched your search
@@ -154,6 +154,73 @@ function StatusCard() {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</StatRow>
|
</StatRow>
|
||||||
|
<StatRow label={tt.lastActions} className="sm:col-span-2">
|
||||||
|
<div className="flex flex-col gap-2 text-xs">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{tt.llmSuggested}</div>
|
||||||
|
{status.last_consolidation_suggestions.consolidations.length ===
|
||||||
|
0 &&
|
||||||
|
status.last_consolidation_suggestions.prunings.length === 0 ? (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{tt.noActions}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<ul className="text-muted-foreground mt-0.5 list-disc pl-4">
|
||||||
|
{status.last_consolidation_suggestions.consolidations.map(
|
||||||
|
(c, i) => (
|
||||||
|
<li key={`c${i}`}>
|
||||||
|
{(c.from ?? "?") + " → " + (c.into ?? "?")}
|
||||||
|
{c.reason ? `(${c.reason})` : ""}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{status.last_consolidation_suggestions.prunings.map(
|
||||||
|
(p, i) => (
|
||||||
|
<li key={`p${i}`}>
|
||||||
|
{p.name ?? "?"}
|
||||||
|
{p.reason ? `(${p.reason})` : ""}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{tt.actuallyDid}</div>
|
||||||
|
{status.last_actual_actions.length === 0 ? (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{tt.noActions}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<ul className="mt-0.5 flex flex-col gap-1">
|
||||||
|
{status.last_actual_actions.map((a, i) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className="flex flex-wrap items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<span className="font-mono">{a.tool ?? "?"}</span>
|
||||||
|
{typeof a.args?.name === "string" && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{String(a.args.name)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
a.status === "error"
|
||||||
|
? "destructive"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
className="px-1 text-[10px]"
|
||||||
|
>
|
||||||
|
{a.status}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StatRow>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -11,6 +11,29 @@ export interface CuratorConfig {
|
|||||||
curator: CuratorSubConfig;
|
curator: CuratorSubConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CuratorAction {
|
||||||
|
tool: string | null;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
status: string;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CuratorConsolidation {
|
||||||
|
from?: string;
|
||||||
|
into?: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CuratorPruning {
|
||||||
|
name?: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CuratorSuggestions {
|
||||||
|
consolidations: CuratorConsolidation[];
|
||||||
|
prunings: CuratorPruning[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface CuratorStatus {
|
export interface CuratorStatus {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
run_count: number;
|
run_count: number;
|
||||||
@@ -23,6 +46,10 @@ export interface CuratorStatus {
|
|||||||
stale: number;
|
stale: number;
|
||||||
archived: number;
|
archived: number;
|
||||||
};
|
};
|
||||||
|
/** 上次运行 LLM 自述的建议(它"说"要做什么)。 */
|
||||||
|
last_consolidation_suggestions: CuratorSuggestions;
|
||||||
|
/** 上次运行的真实工具调用记录(它"实际"做了什么)。 */
|
||||||
|
last_actual_actions: CuratorAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CuratorLogRecord {
|
export interface CuratorLogRecord {
|
||||||
|
|||||||
@@ -557,6 +557,10 @@ export const enUS: Translations = {
|
|||||||
total: "Total",
|
total: "Total",
|
||||||
runNow: "Run now",
|
runNow: "Run now",
|
||||||
runStarted: "Background run started",
|
runStarted: "Background run started",
|
||||||
|
lastActions: "Last run",
|
||||||
|
llmSuggested: "LLM suggested (self-report)",
|
||||||
|
actuallyDid: "Actually done (tool calls)",
|
||||||
|
noActions: "None",
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
title: "Configuration",
|
title: "Configuration",
|
||||||
|
|||||||
@@ -482,6 +482,10 @@ export interface Translations {
|
|||||||
total: string;
|
total: string;
|
||||||
runNow: string;
|
runNow: string;
|
||||||
runStarted: string;
|
runStarted: string;
|
||||||
|
lastActions: string;
|
||||||
|
llmSuggested: string;
|
||||||
|
actuallyDid: string;
|
||||||
|
noActions: string;
|
||||||
};
|
};
|
||||||
config: {
|
config: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -533,6 +533,10 @@ export const zhCN: Translations = {
|
|||||||
total: "合计",
|
total: "合计",
|
||||||
runNow: "立即执行",
|
runNow: "立即执行",
|
||||||
runStarted: "已触发后台执行",
|
runStarted: "已触发后台执行",
|
||||||
|
lastActions: "最近一次运行",
|
||||||
|
llmSuggested: "LLM 建议(自述)",
|
||||||
|
actuallyDid: "实际执行(工具调用)",
|
||||||
|
noActions: "无",
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
title: "配置",
|
title: "配置",
|
||||||
|
|||||||
+66
-49
@@ -69,8 +69,25 @@ _enabled_skills_refresh_version = 0
|
|||||||
_enabled_skills_refresh_event = threading.Event()
|
_enabled_skills_refresh_event = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_archived_skills(skills: list[Skill]) -> list[Skill]:
|
||||||
|
"""从注入提示词的技能列表里剔除已归档(usage state=archived)的技能。
|
||||||
|
|
||||||
|
技能归档是软状态:SKILL.md 仍留在 custom/<name>/、enabled 标志不变,所以
|
||||||
|
load_skills(enabled_only=True) 仍会返回它。但归档技能不应再出现在 agent 的系统
|
||||||
|
提示词里 —— 对 agent 而言它应等同于"不存在"。失败时不过滤(返回原列表)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from deerflow.skills.usage import STATE_ARCHIVED, all_entries
|
||||||
|
|
||||||
|
entries = all_entries()
|
||||||
|
return [s for s in skills if entries.get(s.name, {}).get("state") != STATE_ARCHIVED]
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to filter archived skills for prompt injection", exc_info=True)
|
||||||
|
return skills
|
||||||
|
|
||||||
|
|
||||||
def _load_enabled_skills_sync() -> list[Skill]:
|
def _load_enabled_skills_sync() -> list[Skill]:
|
||||||
return list(get_or_new_skill_storage().load_skills(enabled_only=True))
|
return _drop_archived_skills(list(get_or_new_skill_storage().load_skills(enabled_only=True)))
|
||||||
|
|
||||||
|
|
||||||
def _start_enabled_skills_refresh_thread() -> None:
|
def _start_enabled_skills_refresh_thread() -> None:
|
||||||
@@ -171,7 +188,7 @@ def _get_enabled_skills_for_config(app_config: AppConfig | None = None) -> list[
|
|||||||
"""
|
"""
|
||||||
if app_config is None:
|
if app_config is None:
|
||||||
return _get_enabled_skills()
|
return _get_enabled_skills()
|
||||||
return list(get_or_new_skill_storage(app_config=app_config).load_skills(enabled_only=True))
|
return _drop_archived_skills(list(get_or_new_skill_storage(app_config=app_config).load_skills(enabled_only=True)))
|
||||||
|
|
||||||
|
|
||||||
def _skill_mutability_label(category: SkillCategory | str) -> str:
|
def _skill_mutability_label(category: SkillCategory | str) -> str:
|
||||||
@@ -479,11 +496,11 @@ You: "Deploying to staging..." [proceed]
|
|||||||
|
|
||||||
{skills_section}
|
{skills_section}
|
||||||
|
|
||||||
{deferred_tools_section}
|
{deferred_tools_section}
|
||||||
|
|
||||||
{writing_mode_section}
|
{writing_mode_section}
|
||||||
|
|
||||||
{subagent_section}
|
{subagent_section}
|
||||||
|
|
||||||
<working_directory existed="true">
|
<working_directory existed="true">
|
||||||
- User uploads: `/mnt/user-data/uploads` - Files uploaded by the user (automatically listed in context)
|
- User uploads: `/mnt/user-data/uploads` - Files uploaded by the user (automatically listed in context)
|
||||||
@@ -796,17 +813,17 @@ def _build_custom_mounts_section(*, app_config: AppConfig | None = None) -> str:
|
|||||||
return f"\n**Custom Mounted Directories:**\n{mounts_list}\n- If the user needs files outside `/mnt/user-data`, use these absolute container paths directly when they match the requested directory"
|
return f"\n**Custom Mounted Directories:**\n{mounts_list}\n- If the user needs files outside `/mnt/user-data`, use these absolute container paths directly when they match the requested directory"
|
||||||
|
|
||||||
|
|
||||||
def apply_prompt_template(
|
def apply_prompt_template(
|
||||||
subagent_enabled: bool = False,
|
subagent_enabled: bool = False,
|
||||||
max_concurrent_subagents: int = 3,
|
max_concurrent_subagents: int = 3,
|
||||||
*,
|
*,
|
||||||
agent_id: str | None = None,
|
agent_id: str | None = None,
|
||||||
agent_name: str | None = None,
|
agent_name: str | None = None,
|
||||||
writing_mode: bool = False,
|
writing_mode: bool = False,
|
||||||
writing_artifact_path: str | None = None,
|
writing_artifact_path: str | None = None,
|
||||||
available_skills: set[str] | None = None,
|
available_skills: set[str] | None = None,
|
||||||
app_config: AppConfig | None = None,
|
app_config: AppConfig | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
storage_agent_id = agent_id if agent_id is not None else agent_name
|
storage_agent_id = agent_id if agent_id is not None else agent_name
|
||||||
|
|
||||||
# Get memory context
|
# Get memory context
|
||||||
@@ -838,33 +855,33 @@ def apply_prompt_template(
|
|||||||
skills_section = get_skills_prompt_section(available_skills, app_config=app_config)
|
skills_section = get_skills_prompt_section(available_skills, app_config=app_config)
|
||||||
|
|
||||||
# Get deferred tools section (tool_search)
|
# Get deferred tools section (tool_search)
|
||||||
deferred_tools_section = get_deferred_tools_prompt_section(app_config=app_config)
|
deferred_tools_section = get_deferred_tools_prompt_section(app_config=app_config)
|
||||||
current_writing_artifact = (
|
current_writing_artifact = (
|
||||||
f"\n**Current Markdown Artifact**\n- Continue editing this existing Markdown file unless the user explicitly asks for a new document: `{writing_artifact_path}`\n"
|
f"\n**Current Markdown Artifact**\n- Continue editing this existing Markdown file unless the user explicitly asks for a new document: `{writing_artifact_path}`\n"
|
||||||
if writing_artifact_path
|
if writing_artifact_path
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
writing_mode_section = (
|
writing_mode_section = (
|
||||||
f"""
|
f"""
|
||||||
<writing_mode>
|
<writing_mode>
|
||||||
Writing mode is enabled for this run.
|
Writing mode is enabled for this run.
|
||||||
|
|
||||||
**Mandatory Output Contract**
|
**Mandatory Output Contract**
|
||||||
- Treat the user's request as a writing/report-generation task.
|
- Treat the user's request as a writing/report-generation task.
|
||||||
- Your final deliverable MUST be a Markdown file, not only an inline chat answer.
|
- Your final deliverable MUST be a Markdown file, not only an inline chat answer.
|
||||||
- If a current Markdown artifact is provided below, you MUST revise that existing file rather than creating a new file, unless the user explicitly asks for a separate/new document.
|
- If a current Markdown artifact is provided below, you MUST revise that existing file rather than creating a new file, unless the user explicitly asks for a separate/new document.
|
||||||
- When revising an existing Markdown artifact, read the file first if needed, then overwrite the same path with the complete updated Markdown content.
|
- When revising an existing Markdown artifact, read the file first if needed, then overwrite the same path with the complete updated Markdown content.
|
||||||
- Write the deliverable to `/mnt/user-data/outputs/` with a clear `.md` filename, for example `/mnt/user-data/outputs/report.md`.
|
- Write the deliverable to `/mnt/user-data/outputs/` with a clear `.md` filename, for example `/mnt/user-data/outputs/report.md`.
|
||||||
- Use `write_file` or an equivalent file-writing tool to save the complete Markdown content.
|
- Use `write_file` or an equivalent file-writing tool to save the complete Markdown content.
|
||||||
- After writing the file, call `present_files` with the Markdown file path so the user can open it as an artifact/canvas.
|
- After writing the file, call `present_files` with the Markdown file path so the user can open it as an artifact/canvas.
|
||||||
- The final chat response should be brief and point to the generated Markdown artifact. Do not duplicate the full report inline unless the user explicitly asks.
|
- The final chat response should be brief and point to the generated Markdown artifact. Do not duplicate the full report inline unless the user explicitly asks.
|
||||||
- If you revise an existing Markdown artifact, update the Markdown file and present it again.
|
- If you revise an existing Markdown artifact, update the Markdown file and present it again.
|
||||||
{current_writing_artifact}
|
{current_writing_artifact}
|
||||||
</writing_mode>
|
</writing_mode>
|
||||||
"""
|
"""
|
||||||
if writing_mode
|
if writing_mode
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build ACP agent section only if ACP agents are configured
|
# Build ACP agent section only if ACP agents are configured
|
||||||
acp_section = _build_acp_section(app_config=app_config)
|
acp_section = _build_acp_section(app_config=app_config)
|
||||||
@@ -878,10 +895,10 @@ Writing mode is enabled for this run.
|
|||||||
prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
||||||
agent_name=agent_name or storage_agent_id or "CM 助手",
|
agent_name=agent_name or storage_agent_id or "CM 助手",
|
||||||
soul=get_agent_soul(storage_agent_id),
|
soul=get_agent_soul(storage_agent_id),
|
||||||
skills_section=skills_section,
|
skills_section=skills_section,
|
||||||
deferred_tools_section=deferred_tools_section,
|
deferred_tools_section=deferred_tools_section,
|
||||||
writing_mode_section=writing_mode_section,
|
writing_mode_section=writing_mode_section,
|
||||||
memory_context=memory_context,
|
memory_context=memory_context,
|
||||||
network_policy_section=network_policy_section,
|
network_policy_section=network_policy_section,
|
||||||
subagent_section=subagent_section,
|
subagent_section=subagent_section,
|
||||||
subagent_reminder=subagent_reminder,
|
subagent_reminder=subagent_reminder,
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ def should_run_now(user_id: str) -> bool:
|
|||||||
last_dt = datetime.fromisoformat(last_run)
|
last_dt = datetime.fromisoformat(last_run)
|
||||||
return datetime.now(UTC) >= last_dt + timedelta(hours=interval_hours)
|
return datetime.now(UTC) >= last_dt + timedelta(hours=interval_hours)
|
||||||
except Exception:
|
except Exception:
|
||||||
return True
|
return False
|
||||||
|
|
||||||
|
|
||||||
def apply_automatic_transitions(user_id: str, now: datetime | None = None) -> dict:
|
def apply_automatic_transitions(user_id: str, now: datetime | None = None) -> dict:
|
||||||
@@ -954,4 +954,8 @@ def get_curator_status(user_id: str) -> dict:
|
|||||||
"stale": counts[STATE_STALE],
|
"stale": counts[STATE_STALE],
|
||||||
"archived": counts[STATE_ARCHIVED],
|
"archived": counts[STATE_ARCHIVED],
|
||||||
},
|
},
|
||||||
|
# 上次运行的两份记录:LLM 自述的建议 + 实际发生的工具调用,供前端对照展示。
|
||||||
|
"last_consolidation_suggestions": state.get("last_consolidation_suggestions")
|
||||||
|
or {"consolidations": [], "prunings": []},
|
||||||
|
"last_actual_actions": state.get("last_actual_actions") or [],
|
||||||
}
|
}
|
||||||
+84
-5
@@ -59,6 +59,66 @@ def _load_agent_skill_whitelist(agent_id: str | None) -> set[str] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_effective_user_id() -> str | None:
|
||||||
|
"""当前请求/线程绑定的用户 id;解析失败时返回 None。"""
|
||||||
|
try:
|
||||||
|
from deerflow.runtime.user_context import get_effective_user_id
|
||||||
|
|
||||||
|
return get_effective_user_id()
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to resolve effective user id for skill_list", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _run_coro_blocking(coro):
|
||||||
|
"""在同步工具里把一个协程跑到完成。
|
||||||
|
|
||||||
|
``skill_list`` 是同步 ``@tool``:LangChain 通常在线程池里执行它(无运行中的
|
||||||
|
事件循环),但 curator 评审会在已有事件循环的线程里调用它。两种情况都要能
|
||||||
|
拿到 ``SkillStore`` 的异步查询结果 —— 检测到运行中的事件循环就转交独立线程
|
||||||
|
执行 ``asyncio.run``,否则直接跑。
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.get_running_loop()
|
||||||
|
in_loop = True
|
||||||
|
except RuntimeError:
|
||||||
|
in_loop = False
|
||||||
|
|
||||||
|
if in_loop:
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||||
|
return pool.submit(asyncio.run, coro).result()
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_owned_skill_names(user_id: str | None) -> set[str] | None:
|
||||||
|
"""返回 ``owner_user_id == user_id`` 的自定义技能名集合。
|
||||||
|
|
||||||
|
用于默认 lead agent 的可见性过滤:只算"用户自己拥有的"自定义技能,不含
|
||||||
|
其他用户发布的。返回 ``None`` 表示 ``SkillStore`` 不可用(未配置数据库
|
||||||
|
等)—— 调用方应降级为"放行全部自定义技能",避免离线无库部署下技能凭空
|
||||||
|
消失。
|
||||||
|
"""
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from deerflow.persistence.engine import get_session_factory
|
||||||
|
from deerflow.persistence.skills import make_skill_store
|
||||||
|
|
||||||
|
factory = get_session_factory()
|
||||||
|
if factory is None:
|
||||||
|
return None
|
||||||
|
store = make_skill_store(factory)
|
||||||
|
records = _run_coro_blocking(store.list_visible(user_id))
|
||||||
|
return {r["name"] for r in records if r.get("owner_user_id") == user_id}
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to load owned skill names for skill_list filter", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@tool("skill_view")
|
@tool("skill_view")
|
||||||
def skill_view_tool(name: str) -> str:
|
def skill_view_tool(name: str) -> str:
|
||||||
"""Read the full SKILL.md content of a skill by name.
|
"""Read the full SKILL.md content of a skill by name.
|
||||||
@@ -106,28 +166,47 @@ def skill_list_tool(runtime: ToolRuntime) -> str:
|
|||||||
under the ``skills:`` field — the agent only sees what it was wired to
|
under the ``skills:`` field — the agent only sees what it was wired to
|
||||||
use.
|
use.
|
||||||
|
|
||||||
For the **default lead agent** (no agent_id in runtime), there is no
|
For the **default lead agent** (no agent_id in runtime), the result is
|
||||||
per-agent whitelist and every visible skill is returned.
|
filtered to non-archived skills that are either built-in (public) or
|
||||||
|
owned by the current user. Other users' published custom skills and
|
||||||
|
archived skills are excluded.
|
||||||
|
|
||||||
Returns a JSON array with fields: name, description, category, enabled,
|
Returns a JSON array with fields: name, description, category, enabled,
|
||||||
state, use_count, view_count, patch_count, last_activity_at, pinned,
|
state, use_count, view_count, patch_count, last_activity_at, pinned,
|
||||||
source.
|
source.
|
||||||
"""
|
"""
|
||||||
from deerflow.skills.storage import get_or_new_skill_storage
|
from deerflow.skills.storage import get_or_new_skill_storage
|
||||||
from deerflow.skills.usage import all_entries, latest_activity_at
|
from deerflow.skills.types import SkillCategory
|
||||||
|
from deerflow.skills.usage import STATE_ARCHIVED, all_entries, latest_activity_at
|
||||||
|
|
||||||
agent_id = _resolve_active_agent_id(runtime)
|
agent_id = _resolve_active_agent_id(runtime)
|
||||||
allowed = _load_agent_skill_whitelist(agent_id)
|
allowed = _load_agent_skill_whitelist(agent_id)
|
||||||
|
|
||||||
|
# 默认 lead agent 没有 per-agent 白名单 —— 改为只暴露"未归档"且"内置或属于
|
||||||
|
# 当前用户"的技能。owned 为 None 表示 SkillStore 不可用,降级放行全部自定义
|
||||||
|
# 技能(见 _load_owned_skill_names)。
|
||||||
|
owned: set[str] | None = None
|
||||||
|
if allowed is None:
|
||||||
|
owned = _load_owned_skill_names(_resolve_effective_user_id())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
storage = get_or_new_skill_storage()
|
storage = get_or_new_skill_storage()
|
||||||
skills = storage.load_skills(enabled_only=False)
|
skills = storage.load_skills(enabled_only=False)
|
||||||
usage = all_entries()
|
usage = all_entries()
|
||||||
result = []
|
result = []
|
||||||
for skill in skills:
|
for skill in skills:
|
||||||
if allowed is not None and skill.name not in allowed:
|
|
||||||
continue
|
|
||||||
entry = usage.get(skill.name, {})
|
entry = usage.get(skill.name, {})
|
||||||
|
if allowed is not None:
|
||||||
|
# 自定义 agent:严格按 config.yaml 的 skills 白名单过滤。
|
||||||
|
if skill.name not in allowed:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 默认 agent:跳过已归档技能。
|
||||||
|
if entry.get("state") == STATE_ARCHIVED:
|
||||||
|
continue
|
||||||
|
# 自定义技能仅当属于当前用户时可见;内置 public 技能始终可见。
|
||||||
|
if skill.category == SkillCategory.CUSTOM and owned is not None and skill.name not in owned:
|
||||||
|
continue
|
||||||
result.append({
|
result.append({
|
||||||
"name": skill.name,
|
"name": skill.name,
|
||||||
"description": skill.description,
|
"description": skill.description,
|
||||||
|
|||||||
@@ -84,12 +84,26 @@ def get_available_tools(
|
|||||||
builtin_tools = BUILTIN_TOOLS.copy()
|
builtin_tools = BUILTIN_TOOLS.copy()
|
||||||
if not is_scheduled_run:
|
if not is_scheduled_run:
|
||||||
builtin_tools.append(scheduled_task_tool)
|
builtin_tools.append(scheduled_task_tool)
|
||||||
skill_evolution_config = getattr(config, "skill_evolution", None)
|
# skill_list / skill_view 是只读工具(发现、查看技能),与"自我进化"写能力无关,
|
||||||
|
# 始终提供给 agent。
|
||||||
|
from deerflow.tools.builtins.skill_tools import skill_list_tool, skill_view_tool
|
||||||
|
|
||||||
|
builtin_tools.extend([skill_list_tool, skill_view_tool])
|
||||||
|
|
||||||
|
# skill_manage 能创建/修改/删除技能,属于"自我进化"写能力,按 skill_evolution.enabled
|
||||||
|
# 门控。该开关用户可在 per-user 配置里覆盖,所以按"生效配置(含 per-user 覆盖)"判断 ——
|
||||||
|
# Gateway 每次 run 重建 agent,用户改了配置下次 run 即生效;解析失败退回全局 config.yaml。
|
||||||
|
try:
|
||||||
|
from deerflow.config.skill_evolution_runtime import load_skill_evolution_config_for_user
|
||||||
|
from deerflow.runtime.user_context import get_effective_user_id
|
||||||
|
|
||||||
|
skill_evolution_config = load_skill_evolution_config_for_user(get_effective_user_id())
|
||||||
|
except Exception: # noqa: BLE001 - 解析失败不能阻断 agent 构建
|
||||||
|
skill_evolution_config = getattr(config, "skill_evolution", None)
|
||||||
if getattr(skill_evolution_config, "enabled", False):
|
if getattr(skill_evolution_config, "enabled", False):
|
||||||
from deerflow.tools.builtins.skill_tools import skill_list_tool, skill_view_tool
|
|
||||||
from deerflow.tools.skill_manage_tool import skill_manage_tool
|
from deerflow.tools.skill_manage_tool import skill_manage_tool
|
||||||
|
|
||||||
builtin_tools.extend([skill_list_tool, skill_view_tool, skill_manage_tool])
|
builtin_tools.append(skill_manage_tool)
|
||||||
|
|
||||||
# Add subagent tools only if enabled via runtime parameter
|
# Add subagent tools only if enabled via runtime parameter
|
||||||
if subagent_enabled:
|
if subagent_enabled:
|
||||||
|
|||||||
Reference in new issue
Block a user