feat(skills): 自我进化 - 软归档/恢复 + Agent 归属 + 状态分层 UI
后端:
- 新增 PUT /api/skills/custom/{name}/state 与 /pinned 软归档/恢复 + 钉选 API
- skill_manage 工具: agent 创建写 DB 归属; 删除清理 .usage.json + DB row + cascade 引用; 加 ownership 预校验
- curator 全部软归档(文件保留在 custom/), 加 reason 必填(中文), 修 user_context 跨线程传播 / 协程泄漏 / archive 文件不再移动
- LLM 评审接 ToolErrorHandlingMiddleware, 工具异常自动重试不中止本轮; 替换 create_react_agent 为 create_agent
- skill_list 按当前 agent config.yaml 的 skills 字段过滤(custom agent), lead agent 仍可见全部
- 自我进化提示词对齐 Hermes 风格: 主动询问保存, reason 强制中文且面向用户
- 控制台日志增强: 自动归档/标过时/LLM 合并/删减建议 都按条目输出原因
前端:
- 技能管理页 Tab 改「全部 / 我的」, 正确包含他人发布的 custom 技能(修复 isCustom 互斥 bug)
- 新增「过时 N / 归档 N」右侧抽屉(恢复/查看/立即归档 ⋯ 菜单)
- 新增徽章: 钉选 / 来源(Agent 创建 / 用户上传) / 使用次数展示
- 内置技能徽章修复(原 isBuiltin && isCustom 永远 false)
- 自我进化日志 run_started sentinel 在前端展示为中文分隔条
- 移除冗余 enabled 开关(归档已覆盖此能力, 减少术语)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f8bc74b27f
commit
62d525dae7
16 files changed
+1270
-97
No files matched your search
@@ -641,6 +641,96 @@ async def update_custom_skill_published(
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update published state: {str(e)}")
|
||||
|
||||
|
||||
_ALLOWED_USER_STATES = {"active", "archived"}
|
||||
|
||||
|
||||
class SkillStateRequest(BaseModel):
|
||||
"""Request body for switching a custom skill's lifecycle state."""
|
||||
|
||||
state: str = Field(..., description="New state: 'active' (restore) or 'archived' (soft archive)")
|
||||
|
||||
|
||||
class SkillPinRequest(BaseModel):
|
||||
"""Request body for pinning / unpinning a custom skill."""
|
||||
|
||||
pinned: bool = Field(..., description="Whether to pin (true) so the curator never auto-archives this skill")
|
||||
|
||||
|
||||
def _reload_skill_response(skill_name: str, ownership: dict[str, Any], config: AppConfig) -> SkillResponse:
|
||||
"""Reload a single skill from disk and return its API response form."""
|
||||
skills = get_or_new_skill_storage(app_config=config).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 on disk")
|
||||
return _skill_to_response(skill, ownership)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/skills/custom/{skill_name}/state",
|
||||
response_model=SkillResponse,
|
||||
summary="Update Custom Skill Lifecycle State",
|
||||
description="Restore (active) or soft-archive a custom skill. Only the owner or admins may invoke.",
|
||||
)
|
||||
async def update_custom_skill_state(
|
||||
skill_name: str,
|
||||
body: SkillStateRequest,
|
||||
request: Request,
|
||||
config: AppConfig = Depends(get_config),
|
||||
skill_store: SkillStore = Depends(get_skill_store),
|
||||
) -> SkillResponse:
|
||||
try:
|
||||
skill_name = skill_name.replace("\r\n", "").replace("\n", "")
|
||||
if body.state not in _ALLOWED_USER_STATES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"state must be one of {sorted(_ALLOWED_USER_STATES)}",
|
||||
)
|
||||
user_id = _current_user_id(request)
|
||||
is_admin = _is_admin_user(request)
|
||||
record, _mode = await _get_editable_skill_or_raise(skill_store, skill_name, user_id, is_admin=is_admin)
|
||||
|
||||
from deerflow.skills.usage import set_state as _usage_set_state
|
||||
|
||||
_usage_set_state(skill_name, body.state)
|
||||
await refresh_skills_system_prompt_cache_async()
|
||||
return _reload_skill_response(skill_name, record, config)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to update state for %s: %s", skill_name, e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update state: {e}")
|
||||
|
||||
|
||||
@router.put(
|
||||
"/skills/custom/{skill_name}/pinned",
|
||||
response_model=SkillResponse,
|
||||
summary="Pin or Unpin a Custom Skill",
|
||||
description="Pin a skill so the curator never auto-archives it. Only the owner or admins may invoke.",
|
||||
)
|
||||
async def update_custom_skill_pinned(
|
||||
skill_name: str,
|
||||
body: SkillPinRequest,
|
||||
request: Request,
|
||||
config: AppConfig = Depends(get_config),
|
||||
skill_store: SkillStore = Depends(get_skill_store),
|
||||
) -> SkillResponse:
|
||||
try:
|
||||
skill_name = skill_name.replace("\r\n", "").replace("\n", "")
|
||||
user_id = _current_user_id(request)
|
||||
is_admin = _is_admin_user(request)
|
||||
record, _mode = await _get_editable_skill_or_raise(skill_store, skill_name, user_id, is_admin=is_admin)
|
||||
|
||||
from deerflow.skills.usage import set_pinned as _usage_set_pinned
|
||||
|
||||
_usage_set_pinned(skill_name, body.pinned)
|
||||
return _reload_skill_response(skill_name, record, config)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to update pinned for %s: %s", skill_name, e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update pinned: {e}")
|
||||
|
||||
|
||||
@router.get("/skills/custom/{skill_name}/history", response_model=CustomSkillHistoryResponse, summary="Get Custom Skill History")
|
||||
async def get_custom_skill_history(skill_name: str, config: AppConfig = Depends(get_config)) -> CustomSkillHistoryResponse:
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user