管理员智能体/技能管理 + 跨标签登录同步 + 上游响应异常重试

- 「设置和更多」新增「智能体管理」「技能管理」入口,与定时任务管理同级;
  独立的 AdminAgentsPage/AdminSkillsPage 复用 AgentCard/SkillRow,
  管理员可查看并编辑/删除所有用户的智能体与技能。
- 后端 /api/agents、/api/skills 增加 ?admin=true 越权列表;agents 端
  放宽 _get_editable_or_raise 让管理员可编辑/删除任意智能体;新增
  list_all/update_any/delete_any(agents)与 list_all(skills)。
- 新增 AuthSyncWatcher:监听 localStorage["deerflow.auth"] 的 storage
  事件,user_id 变化时自动 reload,修复多标签切换登录后老页面点击
  「新对话」偶发 404 的问题。
- LLMErrorHandlingMiddleware 将 bad_response_status_code / JSON
  decode 等上游网关乱码识别为可重试的 upstream 类故障。
- 侧边栏「任务管理」改名为「定时任务」。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.7 committed 2026-05-27 11:13:29 +08:00
1 parent 1642c3e694
commit eda0ccffdb
23 files changed
+660 -115

No files matched your search

@@ -695,11 +695,25 @@ async def list_skills(
tag_store: TagStore = Depends(get_tag_store),
search: str | None = Query(default=None, description="Case-insensitive fuzzy match on skill name"),
tag_ids: list[str] | None = Query(default=None, description="Keep skills carrying any of these tags (OR)"),
admin: bool = Query(
default=False,
description=(
"Admin-only override. When ``true`` and the caller is a system admin, "
"every custom skill is returned (including unpublished skills owned by "
"other users) regardless of the usual ownership filter."
),
),
) -> SkillsListResponse:
try:
user_id = _current_user_id(request)
if admin and not _is_admin_user(request):
raise HTTPException(status_code=403, detail="Only system admins can list all skills")
all_skills = get_or_new_skill_storage(app_config=config).load_skills(enabled_only=False)
ownership = await _build_ownership_index(skill_store, user_id)
if admin:
ownership_records = await skill_store.list_all()
ownership = {record["name"]: record for record in ownership_records}
else:
ownership = await _build_ownership_index(skill_store, user_id)
display_adapters = await _list_skill_display_adapters([skill.name for skill in all_skills])
responses: list[SkillResponse] = []
for skill in all_skills:
@@ -720,7 +734,11 @@ async def list_skills(
continue
record = ownership.get(skill.name)
if record is None:
continue
if not admin:
continue
# Admin sees every custom skill on disk even without an
# ownership row (legacy / orphaned), so they can clean it up.
record = None
responses.append(
_skill_to_response(
skill,
@@ -731,6 +749,8 @@ async def list_skills(
)
responses = await _attach_and_filter_tags(tag_store, responses, search, tag_ids)
return SkillsListResponse(skills=responses)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to load skills: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to load skills: {str(e)}")