feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill - Backend: RBAC system with 4 roles and 23 permissions, seeded on startup - Backend: workspace admin endpoints (list/manage all workspaces) - Backend: admin user management API (CRUD, reset password) - Backend: billing API with subscription plans, usage tracking, rate limiting - Backend: fix system_logs.py UNION query and wrong column references - Backend: WebSocket JWT auth and workspace enforcement - Frontend: sidebar navigation replacing top dropdown menu - Frontend: user management page (Users.vue) for admins - Frontend: enhanced Workspaces.vue with admin table view - Frontend: workspace RBAC computed properties in user store - Android: agent marketplace, billing/subscription UI, onboarding wizard - Android: phone login, analytics tracker, crash handler, network diagnostics - Android: splash screen, encrypted token storage, app update enhancements - Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,26 +110,25 @@ def _build_union_query(
|
||||
AgentExecutionLog.id.label("id"),
|
||||
text("'agent'").label("source"),
|
||||
case(
|
||||
(AgentExecutionLog.status == "failed", "ERROR"),
|
||||
(AgentExecutionLog.status == "completed", "INFO"),
|
||||
(AgentExecutionLog.success == False, "ERROR"),
|
||||
else_="INFO"
|
||||
).label("level"),
|
||||
AgentExecutionLog.user_message.label("message"),
|
||||
AgentExecutionLog.input_text.label("message"),
|
||||
AgentExecutionLog.created_at.label("timestamp"),
|
||||
text("'agent_chat'").label("resource_type"),
|
||||
AgentExecutionLog.agent_id.label("resource_id"),
|
||||
AgentExecutionLog.total_latency_ms.label("duration_ms"),
|
||||
AgentExecutionLog.latency_ms.label("duration_ms"),
|
||||
text("NULL").label("username"),
|
||||
)
|
||||
if level:
|
||||
if level.upper() == "ERROR":
|
||||
q = q.filter(AgentExecutionLog.status == "failed")
|
||||
q = q.filter(AgentExecutionLog.success == False)
|
||||
elif level.upper() == "WARN":
|
||||
q = q.filter(AgentExecutionLog.status == "failed")
|
||||
q = q.filter(AgentExecutionLog.success == False)
|
||||
else:
|
||||
q = q.filter(AgentExecutionLog.status != "failed")
|
||||
q = q.filter(AgentExecutionLog.success == True)
|
||||
if keyword:
|
||||
q = q.filter(AgentExecutionLog.user_message.contains(keyword))
|
||||
q = q.filter(AgentExecutionLog.input_text.contains(keyword))
|
||||
if start_date:
|
||||
q = q.filter(AgentExecutionLog.created_at >= start_date)
|
||||
if end_date:
|
||||
@@ -195,7 +194,6 @@ async def get_system_logs(
|
||||
):
|
||||
"""
|
||||
统一日志查询:跨 execution_logs / agent_execution_logs / agent_llm_logs 联合查询。
|
||||
|
||||
管理员可查全部,普通用户只能查看自己相关的 Agent 执行日志和 LLM 日志。
|
||||
"""
|
||||
_check_admin(current_user)
|
||||
@@ -209,22 +207,91 @@ async def get_system_logs(
|
||||
if getattr(current_user, "role", None) != "admin":
|
||||
user_id = current_user.id
|
||||
|
||||
source_val = source if source else "all"
|
||||
queries = _build_union_query(db, source_val, level, keyword, sd, ed, user_id)
|
||||
results: list[UnifiedLogItem] = []
|
||||
|
||||
if not queries:
|
||||
return []
|
||||
# 1) 工作流执行日志
|
||||
if source in (None, "all", "execution"):
|
||||
q = db.query(ExecutionLog)
|
||||
if level:
|
||||
q = q.filter(ExecutionLog.level == level.upper())
|
||||
if keyword:
|
||||
q = q.filter(ExecutionLog.message.contains(keyword))
|
||||
if sd:
|
||||
q = q.filter(ExecutionLog.timestamp >= sd)
|
||||
if ed:
|
||||
q = q.filter(ExecutionLog.timestamp <= ed)
|
||||
for row in q.order_by(ExecutionLog.timestamp.desc()).offset(skip).limit(limit).all():
|
||||
results.append(UnifiedLogItem(
|
||||
id=row.id,
|
||||
source="execution",
|
||||
level=row.level,
|
||||
message=row.message or "",
|
||||
timestamp=row.timestamp,
|
||||
resource_type=row.node_type,
|
||||
resource_id=row.execution_id,
|
||||
duration_ms=row.duration,
|
||||
))
|
||||
|
||||
# UNION ALL
|
||||
union = queries[0]
|
||||
for q in queries[1:]:
|
||||
union = union.union_all(q)
|
||||
# 2) Agent 执行日志
|
||||
if source in (None, "all", "agent"):
|
||||
q = db.query(AgentExecutionLog)
|
||||
if keyword:
|
||||
q = q.filter(AgentExecutionLog.input_text.contains(keyword))
|
||||
if sd:
|
||||
q = q.filter(AgentExecutionLog.created_at >= sd)
|
||||
if ed:
|
||||
q = q.filter(AgentExecutionLog.created_at <= ed)
|
||||
if user_id:
|
||||
q = q.filter(AgentExecutionLog.user_id == user_id)
|
||||
if level:
|
||||
if level.upper() == "ERROR":
|
||||
q = q.filter(AgentExecutionLog.success == False)
|
||||
elif level.upper() != "WARN":
|
||||
q = q.filter(AgentExecutionLog.success == True)
|
||||
for row in q.order_by(AgentExecutionLog.created_at.desc()).offset(skip).limit(limit).all():
|
||||
results.append(UnifiedLogItem(
|
||||
id=row.id,
|
||||
source="agent",
|
||||
level="ERROR" if not row.success else "INFO",
|
||||
message=row.input_text or "",
|
||||
timestamp=row.created_at,
|
||||
resource_type="agent_chat",
|
||||
resource_id=row.agent_id,
|
||||
duration_ms=row.latency_ms,
|
||||
))
|
||||
|
||||
# 排序 + 分页
|
||||
total = union.count() if hasattr(union, 'count') else 0
|
||||
rows = union.order_by(text("timestamp DESC")).offset(skip).limit(limit).all()
|
||||
# 3) LLM 调用日志
|
||||
if source in (None, "all", "llm"):
|
||||
q = db.query(AgentLLMLog)
|
||||
if keyword:
|
||||
q = q.filter(AgentLLMLog.error_message.contains(keyword))
|
||||
if sd:
|
||||
q = q.filter(AgentLLMLog.created_at >= sd)
|
||||
if ed:
|
||||
q = q.filter(AgentLLMLog.created_at <= ed)
|
||||
if level:
|
||||
if level.upper() == "ERROR":
|
||||
q = q.filter(AgentLLMLog.status == "error")
|
||||
elif level.upper() == "WARN":
|
||||
q = q.filter(AgentLLMLog.status == "rate_limited")
|
||||
else:
|
||||
q = q.filter(AgentLLMLog.status == "success")
|
||||
for row in q.order_by(AgentLLMLog.created_at.desc()).offset(skip).limit(limit).all():
|
||||
msg = row.error_message or f"LLM call: {row.model or 'unknown'} ({row.step_type or '?'})"
|
||||
results.append(UnifiedLogItem(
|
||||
id=row.id,
|
||||
source="llm",
|
||||
level="ERROR" if row.status == "error" else ("WARN" if row.status == "rate_limited" else "INFO"),
|
||||
message=msg,
|
||||
timestamp=row.created_at,
|
||||
resource_type="llm_call",
|
||||
resource_id=row.agent_id,
|
||||
duration_ms=row.latency_ms,
|
||||
))
|
||||
|
||||
return rows
|
||||
# 按时间排序 + 分页
|
||||
results.sort(key=lambda x: x.timestamp or datetime.min, reverse=True)
|
||||
return results[skip:skip + limit]
|
||||
|
||||
|
||||
@router.get("/stats", response_model=LogStatsResponse)
|
||||
@@ -258,7 +325,7 @@ async def get_system_logs_stats(
|
||||
).scalar() or 0
|
||||
agent_errors = db.query(func.count(AgentExecutionLog.id)).filter(
|
||||
AgentExecutionLog.created_at >= today_start,
|
||||
AgentExecutionLog.status == "failed"
|
||||
AgentExecutionLog.success == False
|
||||
).scalar() or 0
|
||||
|
||||
# LLM 调用日志统计
|
||||
|
||||
Reference in New Issue
Block a user