docs/SKILL_NAME_CONFLICT_FIX.md
本文档详细记录了修复 public skill 和 custom skill 同名冲突问题的所有代码改动。
状态: ⚠️ 已知问题保留 - 同名技能冲突问题已识别但暂时保留,后续版本修复
日期: 2026-02-10
当 public skill 和 custom skill 有相同名称(但技能文件内容不同)时,会出现以下问题:
skill_name 作为键{category}:{name} 格式作为配置键,确保唯一性name)的支持category 查询参数来区分同名技能backend/packages/harness/deerflow/config/extensions_config.py)get_skill_key()位置: 第 152-166 行
代码:
@staticmethod
def get_skill_key(skill_name: str, skill_category: str) -> str:
"""Get the key for a skill in the configuration.
Uses format '{category}:{name}' to uniquely identify skills,
allowing public and custom skills with the same name to coexist.
Args:
skill_name: Name of the skill
skill_category: Category of the skill ('public' or 'custom')
Returns:
The skill key in format '{category}:{name}'
"""
return f"{skill_category}:{skill_name}"
作用: 生成组合键,格式为 {category}:{name}
影响:
is_skill_enabled() 和 API 路由使用is_skill_enabled()位置: 第 168-195 行
修改前:
def is_skill_enabled(self, skill_name: str, skill_category: str) -> bool:
skill_config = self.skills.get(skill_name)
if skill_config is None:
return skill_category in ("public", "custom")
return skill_config.enabled
修改后:
def is_skill_enabled(self, skill_name: str, skill_category: str) -> bool:
"""Check if a skill is enabled.
First checks for the new format key '{category}:{name}', then falls back
to the old format '{name}' for backward compatibility.
Args:
skill_name: Name of the skill
skill_category: Category of the skill
Returns:
True if enabled, False otherwise
"""
# Try new format first: {category}:{name}
skill_key = self.get_skill_key(skill_name, skill_category)
skill_config = self.skills.get(skill_key)
if skill_config is not None:
return skill_config.enabled
# Fallback to old format for backward compatibility: {name}
# Only check old format if category is 'public' to avoid conflicts
if skill_category == "public":
skill_config = self.skills.get(skill_name)
if skill_config is not None:
return skill_config.enabled
# Default to enabled for public & custom skills
return skill_category in ("public", "custom")
改动说明:
{category}:{name}影响:
backend/packages/harness/deerflow/skills/loader.py)位置: 第 54-86 行
修改前:
skills = []
# Scan public and custom directories
for category in ["public", "custom"]:
category_path = skills_path / category
# ... 扫描技能目录 ...
skill = parse_skill_file(skill_file, category=category)
if skill:
skills.append(skill)
修改后:
skills = []
category_skill_names = {} # Track skill names per category to detect duplicates
# Scan public and custom directories
for category in ["public", "custom"]:
category_path = skills_path / category
if not category_path.exists() or not category_path.is_dir():
continue
# Initialize tracking for this category
if category not in category_skill_names:
category_skill_names[category] = {}
# Each subdirectory is a potential skill
for skill_dir in category_path.iterdir():
# ... 扫描逻辑 ...
skill = parse_skill_file(skill_file, category=category)
if skill:
# Validate: each category cannot have duplicate skill names
if skill.name in category_skill_names[category]:
existing_path = category_skill_names[category][skill.name]
raise ValueError(
f"Duplicate skill name '{skill.name}' found in {category} category. "
f"Existing: {existing_path}, Duplicate: {skill_file.parent}"
)
category_skill_names[category][skill.name] = str(skill_file.parent)
skills.append(skill)
改动说明:
ValueError,包含详细路径信息影响:
backend/app/gateway/routers/skills.py)_find_skill_by_name()位置: 第 136-173 行
代码:
def _find_skill_by_name(
skills: list[Skill], skill_name: str, category: str | None = None
) -> Skill:
"""Find a skill by name, optionally filtered by category.
Args:
skills: List of all skills
skill_name: Name of the skill to find
category: Optional category filter
Returns:
The found Skill object
Raises:
HTTPException: If skill not found or multiple skills require category
"""
if category:
skill = next((s for s in skills if s.name == skill_name and s.category == category), None)
if skill is None:
raise HTTPException(
status_code=404,
detail=f"Skill '{skill_name}' with category '{category}' not found"
)
return skill
# If no category provided, check if there are multiple skills with the same name
matching_skills = [s for s in skills if s.name == skill_name]
if len(matching_skills) == 0:
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
elif len(matching_skills) > 1:
# Multiple skills with same name - require category
categories = [s.category for s in matching_skills]
raise HTTPException(
status_code=400,
detail=f"Multiple skills found with name '{skill_name}'. Please specify category query parameter. "
f"Available categories: {', '.join(categories)}"
)
return matching_skills[0]
作用:
影响:
GET /api/skills/{skill_name}位置: 第 196-260 行
修改前:
@router.get("/skills/{skill_name}", ...)
async def get_skill(skill_name: str) -> SkillResponse:
skills = load_skills(enabled_only=False)
skill = next((s for s in skills if s.name == skill_name), None)
if skill is None:
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
return _skill_to_response(skill)
修改后:
@router.get(
"/skills/{skill_name}",
response_model=SkillResponse,
summary="Get Skill Details",
description="Retrieve detailed information about a specific skill by its name. "
"If multiple skills share the same name, use category query parameter.",
)
async def get_skill(skill_name: str, category: str | None = None) -> SkillResponse:
try:
skills = load_skills(enabled_only=False)
skill = _find_skill_by_name(skills, skill_name, category)
return _skill_to_response(skill)
except ValueError as e:
# ValueError indicates duplicate skill names in a category
logger.error(f"Invalid skills configuration: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get skill {skill_name}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to get skill: {str(e)}")
改动说明:
category 查询参数_find_skill_by_name() 统一查找逻辑ValueError 处理(重复检查错误)API 变更:
category 参数可选categoryPUT /api/skills/{skill_name}位置: 第 267-388 行
修改前:
@router.put("/skills/{skill_name}", ...)
async def update_skill(skill_name: str, request: SkillUpdateRequest) -> SkillResponse:
skills = load_skills(enabled_only=False)
skill = next((s for s in skills if s.name == skill_name), None)
if skill is None:
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
extensions_config.skills[skill_name] = SkillStateConfig(enabled=request.enabled)
# ... 保存配置 ...
修改后:
@router.put(
"/skills/{skill_name}",
response_model=SkillResponse,
summary="Update Skill",
description="Update a skill's enabled status by modifying the extensions_config.json file. "
"Requires category query parameter to uniquely identify skills with the same name.",
)
async def update_skill(skill_name: str, request: SkillUpdateRequest, category: str | None = None) -> SkillResponse:
try:
# Find the skill to verify it exists
skills = load_skills(enabled_only=False)
skill = _find_skill_by_name(skills, skill_name, category)
# Get or create config path
config_path = ExtensionsConfig.resolve_config_path()
# ... 配置路径处理 ...
# Load current configuration
extensions_config = get_extensions_config()
# Use the new format key: {category}:{name}
skill_key = ExtensionsConfig.get_skill_key(skill.name, skill.category)
extensions_config.skills[skill_key] = SkillStateConfig(enabled=request.enabled)
# Convert to JSON format (preserve MCP servers config)
config_data = {
"mcpServers": {name: server.model_dump() for name, server in extensions_config.mcp_servers.items()},
"skills": {name: {"enabled": skill_config.enabled} for name, skill_config in extensions_config.skills.items()},
}
# Write the configuration to file
with open(config_path, "w") as f:
json.dump(config_data, f, indent=2)
# Reload the extensions config to update the global cache
reload_extensions_config()
# Reload the skills to get the updated status (for API response)
skills = load_skills(enabled_only=False)
updated_skill = next((s for s in skills if s.name == skill.name and s.category == skill.category), None)
if updated_skill is None:
raise HTTPException(
status_code=500,
detail=f"Failed to reload skill '{skill.name}' (category: {skill.category}) after update"
)
logger.info(f"Skill '{skill.name}' (category: {skill.category}) enabled status updated to {request.enabled}")
return _skill_to_response(updated_skill)
except ValueError as e:
# ValueError indicates duplicate skill names in a category
logger.error(f"Invalid skills configuration: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update skill {skill_name}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to update skill: {str(e)}")
改动说明:
category 查询参数_find_skill_by_name() 查找技能ExtensionsConfig.get_skill_key() 存储配置ValueError 处理API 变更:
category 参数可选POST /api/skills/install位置: 第 392-529 行
修改前:
# Check if skill already exists
target_dir = custom_skills_dir / skill_name
if target_dir.exists():
raise HTTPException(status_code=409, detail=f"Skill '{skill_name}' already exists. Please remove it first or use a different name.")
修改后:
# Check if skill directory already exists
target_dir = custom_skills_dir / skill_name
if target_dir.exists():
raise HTTPException(status_code=409, detail=f"Skill directory '{skill_name}' already exists. Please remove it first or use a different name.")
# Check if a skill with the same name already exists in custom category
# This prevents duplicate skill names even if directory names differ
try:
existing_skills = load_skills(enabled_only=False)
duplicate_skill = next(
(s for s in existing_skills if s.name == skill_name and s.category == "custom"),
None
)
if duplicate_skill:
raise HTTPException(
status_code=409,
detail=f"Skill with name '{skill_name}' already exists in custom category "
f"(located at: {duplicate_skill.skill_dir}). Please remove it first or use a different name."
)
except ValueError as e:
# ValueError indicates duplicate skill names in configuration
# This should not happen during installation, but handle it gracefully
logger.warning(f"Skills configuration issue detected during installation: {e}")
raise HTTPException(
status_code=500,
detail=f"Cannot install skill: {str(e)}"
)
改动说明:
ValueError 处理影响:
frontend/src/core/skills/api.ts)enableSkill()位置: 第 11-30 行
修改前:
export async function enableSkill(skillName: string, enabled: boolean) {
const response = await fetch(
`${getBackendBaseURL()}/api/skills/${skillName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
enabled,
}),
},
);
return response.json();
}
修改后:
export async function enableSkill(
skillName: string,
enabled: boolean,
category: string,
) {
const baseURL = getBackendBaseURL();
const skillNameEncoded = encodeURIComponent(skillName);
const categoryEncoded = encodeURIComponent(category);
const url = `${baseURL}/api/skills/${skillNameEncoded}?category=${categoryEncoded}`;
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
enabled,
}),
});
return response.json();
}
改动说明:
category 参数影响:
frontend/src/core/skills/hooks.ts)useEnableSkill()位置: 第 15-33 行
修改前:
export function useEnableSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
skillName,
enabled,
}: {
skillName: string;
enabled: boolean;
}) => {
await enableSkill(skillName, enabled);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
修改后:
export function useEnableSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
skillName,
enabled,
category,
}: {
skillName: string;
enabled: boolean;
category: string;
}) => {
await enableSkill(skillName, enabled, category);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
改动说明:
category 参数到类型定义category 给 enableSkill() API 调用影响:
frontend/src/components/workspace/settings/skill-settings-page.tsx)SkillSettingsList位置: 第 92-119 行
修改前:
{filteredSkills.length > 0 &&
filteredSkills.map((skill) => (
<Item className="w-full" variant="outline" key={skill.name}>
<Switch
checked={skill.enabled}
onCheckedChange={(checked) =>
enableSkill({ skillName: skill.name, enabled: checked })
}
/>
</Item>
))}
修改后:
{filteredSkills.length > 0 &&
filteredSkills.map((skill) => (
<Item
className="w-full"
variant="outline"
key={`${skill.category}:${skill.name}`}
>
<Switch
checked={skill.enabled}
onCheckedChange={(checked) =>
enableSkill({
skillName: skill.name,
enabled: checked,
category: skill.category,
})
}
/>
</Item>
))}
改动说明:
skill.name 改为 ${skill.category}:${skill.name}category 给 enableSkill()影响:
{
"skills": {
"my-skill": {
"enabled": true
}
}
}
{
"skills": {
"public:my-skill": {
"enabled": true
},
"custom:my-skill": {
"enabled": false
}
}
}
新增查询参数:
category (可选): public 或 custom行为变更:
category 参数示例:
# 单个技能(向后兼容)
GET /api/skills/my-skill
# 多个同名技能(必须指定类别)
GET /api/skills/my-skill?category=public
GET /api/skills/my-skill?category=custom
新增查询参数:
category (可选): public 或 custom行为变更:
{category}:{name}category 参数示例:
# 更新 public 技能
PUT /api/skills/my-skill?category=public
Body: { "enabled": true }
# 更新 custom 技能
PUT /api/skills/my-skill?category=custom
Body: { "enabled": false }
ExtensionsConfig.is_skill_enabled() - 支持新格式,向后兼容PUT /api/skills/{skill_name} - 使用新格式键load_skills() - 添加重复检查category 参数enableSkill() - 必须传递 categoryuseEnableSkill() - 类型定义更新SkillSettingsList - React key 和参数传递更新{category}:{name} 格式skill_name 的 API 调用应正常工作(单个技能时)category 参数应正确工作category 应返回 400 错误当前状态: 同名技能冲突问题已识别但暂时保留,后续版本修复
问题表现:
影响范围:
如果需要回滚这些改动:
恢复配置读取逻辑:
# 恢复为仅使用 skill_name
skill_config = self.skills.get(skill_name)
恢复 API 端点:
category 参数移除重复检查:
category_skill_names 跟踪逻辑恢复 API 调用:
// 移除 category 参数
export async function enableSkill(skillName: string, enabled: boolean)
恢复组件:
skill.namecategory 参数传递后端文件: 3 个文件修改
backend/packages/harness/deerflow/config/extensions_config.py: +1 方法,修改 1 方法backend/packages/harness/deerflow/skills/loader.py: +重复检查逻辑backend/app/gateway/routers/skills.py: +1 辅助函数,修改 3 个端点前端文件: 3 个文件修改
frontend/src/core/skills/api.ts: 修改 1 个函数frontend/src/core/skills/hooks.ts: 修改 1 个 hookfrontend/src/components/workspace/settings/skill-settings-page.tsx: 修改组件代码行数:
文档版本: 1.0
最后更新: 2026-02-10
维护者: AI Assistant