Files
deer-flow/docs/SKILL_NAME_CONFLICT_FIX.md
DanielWalnut 76803b826f refactor: split backend into harness (deerflow.*) and app (app.*) (#1131)
* refactor: extract shared utils to break harness→app cross-layer imports

Move _validate_skill_frontmatter to src/skills/validation.py and
CONVERTIBLE_EXTENSIONS + convert_file_to_markdown to src/utils/file_conversion.py.
This eliminates the two reverse dependencies from client.py (harness layer)
into gateway/routers/ (app layer), preparing for the harness/app package split.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: split backend/src into harness (deerflow.*) and app (app.*)

Physically split the monolithic backend/src/ package into two layers:

- **Harness** (`packages/harness/deerflow/`): publishable agent framework
  package with import prefix `deerflow.*`. Contains agents, sandbox, tools,
  models, MCP, skills, config, and all core infrastructure.

- **App** (`app/`): unpublished application code with import prefix `app.*`.
  Contains gateway (FastAPI REST API) and channels (IM integrations).

Key changes:
- Move 13 harness modules to packages/harness/deerflow/ via git mv
- Move gateway + channels to app/ via git mv
- Rename all imports: src.* → deerflow.* (harness) / app.* (app layer)
- Set up uv workspace with deerflow-harness as workspace member
- Update langgraph.json, config.example.yaml, all scripts, Docker files
- Add build-system (hatchling) to harness pyproject.toml
- Add PYTHONPATH=. to gateway startup commands for app.* resolution
- Update ruff.toml with known-first-party for import sorting
- Update all documentation to reflect new directory structure

Boundary rule enforced: harness code never imports from app.
All 429 tests pass. Lint clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add harness→app boundary check test and update docs

Add test_harness_boundary.py that scans all Python files in
packages/harness/deerflow/ and fails if any `from app.*` or
`import app.*` statement is found. This enforces the architectural
rule that the harness layer never depends on the app layer.

Update CLAUDE.md to document the harness/app split architecture,
import conventions, and the boundary enforcement test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add config versioning with auto-upgrade on startup

When config.example.yaml schema changes, developers' local config.yaml
files can silently become outdated. This adds a config_version field and
auto-upgrade mechanism so breaking changes (like src.* → deerflow.*
renames) are applied automatically before services start.

- Add config_version: 1 to config.example.yaml
- Add startup version check warning in AppConfig.from_file()
- Add scripts/config-upgrade.sh with migration registry for value replacements
- Add `make config-upgrade` target
- Auto-run config-upgrade in serve.sh and start-daemon.sh before starting services
- Add config error hints in service failure messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix comments

* fix: update src.* import in test_sandbox_tools_security to deerflow.*

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: handle empty config and search parent dirs for config.example.yaml

Address Copilot review comments on PR #1131:
- Guard against yaml.safe_load() returning None for empty config files
- Search parent directories for config.example.yaml instead of only
  looking next to config.yaml, fixing detection in common setups

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct skills root path depth and config_version type coercion

- loader.py: fix get_skills_root_path() to use 5 parent levels (was 3)
  after harness split, file lives at packages/harness/deerflow/skills/
  so parent×3 resolved to backend/packages/harness/ instead of backend/
- app_config.py: coerce config_version to int() before comparison in
  _check_config_version() to prevent TypeError when YAML stores value
  as string (e.g. config_version: "1")
- tests: add regression tests for both fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update test imports from src.* to deerflow.*/app.* after harness refactor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:55:52 +08:00

866 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技能名称冲突修复 - 代码改动文档
## 概述
本文档详细记录了修复 public skill 和 custom skill 同名冲突问题的所有代码改动。
**状态**: ⚠️ **已知问题保留** - 同名技能冲突问题已识别但暂时保留,后续版本修复
**日期**: 2026-02-10
---
## 问题描述
### 原始问题
当 public skill 和 custom skill 有相同名称(但技能文件内容不同)时,会出现以下问题:
1. **打开冲突**: 打开 public skill 时,同名的 custom skill 也会被打开
2. **关闭冲突**: 关闭 public skill 时,同名的 custom skill 也会被关闭
3. **配置冲突**: 两个技能共享同一个配置键,导致状态互相影响
### 根本原因
- 配置文件中技能状态仅使用 `skill_name` 作为键
- 同名但不同类别的技能无法区分
- 缺少类别级别的重复检查
---
## 解决方案
### 核心思路
1. **组合键存储**: 使用 `{category}:{name}` 格式作为配置键,确保唯一性
2. **向后兼容**: 保持对旧格式(仅 `name`)的支持
3. **重复检查**: 在加载时检查每个类别内是否有重复的技能名称
4. **API 增强**: API 支持可选的 `category` 查询参数来区分同名技能
### 设计原则
- ✅ 最小改动原则
- ✅ 向后兼容
- ✅ 清晰的错误提示
- ✅ 代码复用(提取公共函数)
---
## 详细代码改动
### 一、后端配置层 (`backend/packages/harness/deerflow/config/extensions_config.py`)
#### 1.1 新增方法: `get_skill_key()`
**位置**: 第 152-166 行
**代码**:
```python
@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 路由使用
---
#### 1.2 修改方法: `is_skill_enabled()`
**位置**: 第 168-195 行
**修改前**:
```python
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
```
**修改后**:
```python
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}`
- 向后兼容:如果新格式不存在,检查旧格式(仅 public 类别)
- 保持默认行为:未配置时默认启用
**影响**:
- ✅ 向后兼容:旧配置仍可正常工作
- ✅ 新配置使用组合键,避免冲突
- ✅ 不影响现有调用方
---
### 二、后端技能加载器 (`backend/packages/harness/deerflow/skills/loader.py`)
#### 2.1 添加重复检查逻辑
**位置**: 第 54-86 行
**修改前**:
```python
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)
```
**修改后**:
```python
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`,包含详细路径信息
- 确保每个类别内技能名称唯一
**影响**:
- ✅ 防止配置冲突
- ✅ 清晰的错误提示
- ⚠️ 如果存在重复,加载会失败(这是预期行为)
---
### 三、后端 API 路由 (`backend/app/gateway/routers/skills.py`)
#### 3.1 新增辅助函数: `_find_skill_by_name()`
**位置**: 第 136-173 行
**代码**:
```python
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]
```
**作用**:
- 统一技能查找逻辑
- 支持可选的 category 过滤
- 自动检测同名冲突并提示
**影响**:
- ✅ 减少代码重复(约 30 行)
- ✅ 统一错误处理逻辑
---
#### 3.2 修改端点: `GET /api/skills/{skill_name}`
**位置**: 第 196-260 行
**修改前**:
```python
@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)
```
**修改后**:
```python
@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` 参数可选
- ✅ 如果只有一个同名技能,自动匹配
- ✅ 如果有多个同名技能,要求提供 `category`
---
#### 3.3 修改端点: `PUT /api/skills/{skill_name}`
**位置**: 第 267-388 行
**修改前**:
```python
@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)
# ... 保存配置 ...
```
**修改后**:
```python
@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` 参数可选
- ✅ 配置存储使用新格式键
---
#### 3.4 修改端点: `POST /api/skills/install`
**位置**: 第 392-529 行
**修改前**:
```python
# 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.")
```
**修改后**:
```python
# 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)}"
)
```
**改动说明**:
- 检查目录是否存在(原有逻辑)
- **新增**: 检查 custom 类别中是否已有同名技能(即使目录名不同)
- 添加 `ValueError` 处理
**影响**:
- ✅ 防止安装同名技能
- ✅ 清晰的错误提示
---
### 四、前端 API 层 (`frontend/src/core/skills/api.ts`)
#### 4.1 修改函数: `enableSkill()`
**位置**: 第 11-30 行
**修改前**:
```typescript
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();
}
```
**修改后**:
```typescript
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` 参数
- URL 编码 skillName 和 category
- 将 category 作为查询参数传递
**影响**:
- ✅ 必须传递 category前端已有该信息
- ✅ URL 编码确保特殊字符正确处理
---
### 五、前端 Hooks 层 (`frontend/src/core/skills/hooks.ts`)
#### 5.1 修改 Hook: `useEnableSkill()`
**位置**: 第 15-33 行
**修改前**:
```typescript
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"] });
},
});
}
```
**修改后**:
```typescript
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 调用
**影响**:
- ✅ 类型安全
- ✅ 必须传递 category
---
### 六、前端组件层 (`frontend/src/components/workspace/settings/skill-settings-page.tsx`)
#### 6.1 修改组件: `SkillSettingsList`
**位置**: 第 92-119 行
**修改前**:
```typescript
{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>
))}
```
**修改后**:
```typescript
{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>
))}
```
**改动说明**:
- **关键改动**: React key 从 `skill.name` 改为 `${skill.category}:${skill.name}`
- 传递 `category``enableSkill()`
**影响**:
- ✅ 确保 React key 唯一性(避免同名技能冲突)
- ✅ 正确传递 category 信息
---
## 配置格式变更
### 旧格式(向后兼容)
```json
{
"skills": {
"my-skill": {
"enabled": true
}
}
}
```
### 新格式(推荐)
```json
{
"skills": {
"public:my-skill": {
"enabled": true
},
"custom:my-skill": {
"enabled": false
}
}
}
```
### 迁移说明
-**自动兼容**: 系统会自动识别旧格式
-**无需手动迁移**: 旧配置继续工作
-**新配置使用新格式**: 更新技能状态时自动使用新格式键
---
## API 变更
### GET /api/skills/{skill_name}
**新增查询参数**:
- `category` (可选): `public``custom`
**行为变更**:
- 如果只有一个同名技能,自动匹配(向后兼容)
- 如果有多个同名技能,必须提供 `category` 参数
**示例**:
```bash
# 单个技能(向后兼容)
GET /api/skills/my-skill
# 多个同名技能(必须指定类别)
GET /api/skills/my-skill?category=public
GET /api/skills/my-skill?category=custom
```
### PUT /api/skills/{skill_name}
**新增查询参数**:
- `category` (可选): `public``custom`
**行为变更**:
- 配置存储使用新格式键 `{category}:{name}`
- 如果只有一个同名技能,自动匹配(向后兼容)
- 如果有多个同名技能,必须提供 `category` 参数
**示例**:
```bash
# 更新 public 技能
PUT /api/skills/my-skill?category=public
Body: { "enabled": true }
# 更新 custom 技能
PUT /api/skills/my-skill?category=custom
Body: { "enabled": false }
```
---
## 影响范围
### 后端
1. **配置读取**: `ExtensionsConfig.is_skill_enabled()` - 支持新格式,向后兼容
2. **配置写入**: `PUT /api/skills/{skill_name}` - 使用新格式键
3. **技能加载**: `load_skills()` - 添加重复检查
4. **API 端点**: 3 个端点支持可选的 `category` 参数
### 前端
1. **API 调用**: `enableSkill()` - 必须传递 `category`
2. **Hooks**: `useEnableSkill()` - 类型定义更新
3. **组件**: `SkillSettingsList` - React key 和参数传递更新
### 配置文件
- **格式变更**: 新配置使用 `{category}:{name}` 格式
- **向后兼容**: 旧格式继续支持
- **自动迁移**: 更新时自动使用新格式
---
## 测试建议
### 1. 向后兼容性测试
- [ ] 旧格式配置文件应正常工作
- [ ] 仅使用 `skill_name` 的 API 调用应正常工作(单个技能时)
- [ ] 现有技能状态应保持不变
### 2. 新功能测试
- [ ] public 和 custom 同名技能应能独立控制
- [ ] 打开/关闭一个技能不应影响另一个同名技能
- [ ] API 调用传递 `category` 参数应正确工作
### 3. 错误处理测试
- [ ] public 类别内重复技能名称应报错
- [ ] custom 类别内重复技能名称应报错
- [ ] 多个同名技能时,不提供 `category` 应返回 400 错误
### 4. 安装测试
- [ ] 安装同名技能应被拒绝409 错误)
- [ ] 错误信息应包含现有技能的位置
---
## 已知问题(暂时保留)
### ⚠️ 问题描述
**当前状态**: 同名技能冲突问题已识别但**暂时保留**,后续版本修复
**问题表现**:
- 如果 public 和 custom 目录下存在同名技能,虽然配置已使用组合键区分,但前端 UI 可能仍会出现混淆
- 用户可能无法清楚区分哪个是 public哪个是 custom
**影响范围**:
- 用户体验:可能无法清楚区分同名技能
- 功能:技能状态可以独立控制(已修复)
- 数据:配置正确存储(已修复)
### 后续修复建议
1. **UI 增强**: 在技能列表中明确显示类别标识
2. **名称验证**: 安装时检查是否与 public 技能同名,并给出警告
3. **文档更新**: 说明同名技能的最佳实践
---
## 回滚方案
如果需要回滚这些改动:
### 后端回滚
1. **恢复配置读取逻辑**:
```python
# 恢复为仅使用 skill_name
skill_config = self.skills.get(skill_name)
```
2. **恢复 API 端点**:
- 移除 `category` 参数
- 恢复原有的查找逻辑
3. **移除重复检查**:
- 移除 `category_skill_names` 跟踪逻辑
### 前端回滚
1. **恢复 API 调用**:
```typescript
// 移除 category 参数
export async function enableSkill(skillName: string, enabled: boolean)
```
2. **恢复组件**:
- React key 恢复为 `skill.name`
- 移除 `category` 参数传递
### 配置迁移
- 新格式配置需要手动迁移回旧格式(如果已使用新格式)
- 旧格式配置无需修改
---
## 总结
### 改动统计
- **后端文件**: 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 个 hook
- `frontend/src/components/workspace/settings/skill-settings-page.tsx`: 修改组件
- **代码行数**:
- 新增: ~80 行
- 修改: ~30 行
- 删除: ~0 行(向后兼容)
### 核心改进
1.**配置唯一性**: 使用组合键确保配置唯一
2.**向后兼容**: 旧配置继续工作
3.**重复检查**: 防止配置冲突
4.**代码复用**: 提取公共函数减少重复
5.**错误提示**: 清晰的错误信息
### 注意事项
- ⚠️ **已知问题保留**: UI 区分同名技能的问题待后续修复
-**向后兼容**: 现有配置和 API 调用继续工作
-**最小改动**: 仅修改必要的代码
---
**文档版本**: 1.0
**最后更新**: 2026-02-10
**维护者**: AI Assistant