新增技能目录重新扫描接口与分页,关掉技能安全扫描的配置开关
- 后端新增 POST /api/skills/reconcile-custom:扫描 custom 目录,把磁盘存在但 DB 无记录的技能登记到调用者名下(published=false),附 4 个单元测试 - 前端技能列表加"重新扫描技能目录"按钮,列表分页每页 10 条 - SkillsConfig 新增 security_scan.enabled(默认 true)用于关闭 LLM 安全扫描;scan_skill_content 在开关关闭时直接放行 - 智能体卡片/对话页长名 truncate 显示 + loading 占位 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b6a2c9336e
commit
80fc2a9240
14 files changed
+366
-11
No files matched your search
@@ -161,6 +161,13 @@ class SkillUploadValidateResponse(BaseModel):
|
||||
warnings: list[str] = Field(default_factory=list, description="Non-fatal warnings to surface to the user")
|
||||
|
||||
|
||||
class SkillReconcileResponse(BaseModel):
|
||||
"""Response model for reconciling custom skills found on disk."""
|
||||
|
||||
adopted: list[str] = Field(default_factory=list, description="Skills found on disk that had no DB row; now owned by the caller")
|
||||
already_registered: list[str] = Field(default_factory=list, description="Skills found on disk that already had a DB row (unchanged)")
|
||||
|
||||
|
||||
class CustomSkillContentResponse(SkillResponse):
|
||||
content: str = Field(..., description="Raw SKILL.md content")
|
||||
|
||||
@@ -483,6 +490,54 @@ async def install_skill(
|
||||
raise HTTPException(status_code=500, detail=f"Failed to install skill: {str(e)}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/skills/reconcile-custom",
|
||||
response_model=SkillReconcileResponse,
|
||||
summary="Adopt Orphan Custom Skills",
|
||||
description=(
|
||||
"Scan the on-disk ``skills/custom/`` directory and register any skill that has a "
|
||||
"valid SKILL.md but no DB ownership row. Adopted skills are recorded as owned by "
|
||||
"the calling user with ``published=false``. Skips skills that already have a row "
|
||||
"(regardless of owner). Useful after manually unzipping skills onto the filesystem."
|
||||
),
|
||||
)
|
||||
async def reconcile_custom_skills(
|
||||
request: Request,
|
||||
config: AppConfig = Depends(get_config),
|
||||
skill_store: SkillStore = Depends(get_skill_store),
|
||||
) -> SkillReconcileResponse:
|
||||
try:
|
||||
user_id = _current_user_id(request)
|
||||
storage = get_or_new_skill_storage(app_config=config)
|
||||
custom_skills = [s for s in storage.load_skills(enabled_only=False) if s.category == SkillCategory.CUSTOM]
|
||||
|
||||
adopted: list[str] = []
|
||||
already: list[str] = []
|
||||
for skill in custom_skills:
|
||||
existing = await skill_store.get_any(skill.name)
|
||||
if existing is not None:
|
||||
already.append(skill.name)
|
||||
continue
|
||||
await skill_store.ensure_legacy(
|
||||
{"name": skill.name, "owner_user_id": user_id, "published": False}
|
||||
)
|
||||
try:
|
||||
from deerflow.skills.usage import mark_uploaded
|
||||
mark_uploaded(skill.name)
|
||||
except Exception:
|
||||
pass
|
||||
adopted.append(skill.name)
|
||||
|
||||
if adopted:
|
||||
await refresh_skills_system_prompt_cache_async()
|
||||
return SkillReconcileResponse(adopted=adopted, already_registered=already)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to reconcile custom skills: %s", e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to reconcile custom skills: {e}")
|
||||
|
||||
|
||||
@router.get("/skills/custom", response_model=SkillsListResponse, summary="List Custom Skills")
|
||||
async def list_custom_skills(
|
||||
request: Request,
|
||||
|
||||
Reference in new issue
Block a user