feat: 工具/技能调用统计 + 用户活动审计 + 标签多选筛选

- 任务1: 用户管理/定时任务管理移入左下角菜单, 从侧边栏删除原入口
- 任务2: 管理员查看用户问答(读 LangGraph checkpoint)与记忆, 新增 admin_users 路由与审计页
- 任务3: 标签筛选改为顶部多选标签栏(OR), agents/skills/scheduled_tasks 三端
- 任务4: 新增 tool_call_metrics 表 + ToolMetricsMiddleware + tool-metrics 路由,
  工具/技能调用统计并入大模型统计页, 含错误分类
- 技能识别: lead agent 经 read_file 加载 /mnt/skills/<cat>/<name>/SKILL.md 调用技能,
  中间件扫描工具参数里的 skills/{public,custom}/{name}/ 路径还原 skill_name
- 单元测试 tests/test_tool_metrics_middleware.py (18 项全过)
- 附: SQLite->MySQL 切换后清理脚本

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.7 committed 2026-05-19 00:59:15 +08:00
1 parent e3cde9be25
commit 4940e0d8ed
46 files changed
+3299 -315

No files matched your search

@@ -236,7 +236,7 @@ async def _attach_and_filter_tags(
tag_store: TagStore,
responses: list[SkillResponse],
search: str | None,
tag_id: str | None,
tag_ids: list[str] | None,
) -> list[SkillResponse]:
"""Bulk-attach tags to skill responses, then apply name/tag filters."""
assignments = await tag_store.list_assignments("skill", [r.name for r in responses])
@@ -245,8 +245,9 @@ async def _attach_and_filter_tags(
if search:
needle = search.strip().lower()
responses = [r for r in responses if needle in r.name.lower()]
if tag_id:
responses = [r for r in responses if any(t.get("id") == tag_id for t in r.tags)]
if tag_ids:
wanted = set(tag_ids)
responses = [r for r in responses if any(t.get("id") in wanted for t in r.tags)]
return responses
@@ -262,7 +263,7 @@ async def list_skills(
skill_store: SkillStore = Depends(get_skill_store),
tag_store: TagStore = Depends(get_tag_store),
search: str | None = Query(default=None, description="Case-insensitive fuzzy match on skill name"),
tag_id: str | None = Query(default=None, description="Keep only skills carrying this tag"),
tag_ids: list[str] | None = Query(default=None, description="Keep skills carrying any of these tags (OR)"),
) -> SkillsListResponse:
try:
user_id = _current_user_id(request)
@@ -281,7 +282,7 @@ async def list_skills(
if record is None:
continue
responses.append(_skill_to_response(skill, record))
responses = await _attach_and_filter_tags(tag_store, responses, search, tag_id)
responses = await _attach_and_filter_tags(tag_store, responses, search, tag_ids)
return SkillsListResponse(skills=responses)
except Exception as e:
logger.error(f"Failed to load skills: {e}", exc_info=True)