Files
aiagent/backend/app/api/agent_sessions.py
renjianbo 876789fac1 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>
2026-07-04 01:00:22 +08:00

181 lines
6.0 KiB
Python

"""
Agent 会话管理 API
提供会话重命名、置顶、删除、导出功能
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field
from typing import Optional
from sqlalchemy.orm import Session
import json
from app.core.database import get_db
from app.api.auth import get_current_user
from app.api.deps import get_current_workspace_id
from app.models.user import User
from app.models.agent import Agent
from app.models.agent_session import AgentSession
from app.models.chat_message import ChatMessage
router = APIRouter(prefix="/api/v1/agent-chat", tags=["agent-sessions"])
class UpdateSessionRequest(BaseModel):
title: Optional[str] = Field(default=None, description="会话标题")
is_pinned: Optional[bool] = Field(default=None, description="是否置顶")
class SessionResponse(BaseModel):
id: str
title: Optional[str] = None
is_pinned: bool = False
created_at: Optional[str] = None
updated_at: Optional[str] = None
@router.patch("/{agent_id}/sessions/{session_id}", response_model=SessionResponse)
async def update_session(
agent_id: str,
session_id: str,
req: UpdateSessionRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
workspace_id: str = Depends(get_current_workspace_id),
):
"""更新会话信息(重命名 / 置顶)"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
session = db.query(AgentSession).filter(
AgentSession.id == session_id,
AgentSession.agent_id == agent_id,
).first()
if not session:
session = AgentSession(
id=session_id,
user_id=current_user.id,
agent_id=agent_id,
workspace_id=workspace_id or None,
)
db.add(session)
if req.title is not None:
session.title = req.title
if req.is_pinned is not None:
session.is_pinned = req.is_pinned
db.commit()
db.refresh(session)
return SessionResponse(
id=session.id,
title=session.title,
is_pinned=session.is_pinned,
created_at=session.created_at.isoformat() if session.created_at else None,
updated_at=session.updated_at.isoformat() if session.updated_at else None,
)
@router.delete("/{agent_id}/sessions/{session_id}")
async def delete_session(
agent_id: str,
session_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
workspace_id: str = Depends(get_current_workspace_id),
):
"""删除会话及所有关联消息"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
session_filter = [
AgentSession.id == session_id,
AgentSession.agent_id == agent_id,
]
if workspace_id:
session_filter.append(AgentSession.workspace_id == workspace_id)
db.query(AgentSession).filter(*session_filter).delete()
msg_filter = [
ChatMessage.agent_id == agent_id,
ChatMessage.session_id == session_id,
]
if workspace_id:
msg_filter.append(ChatMessage.workspace_id == workspace_id)
deleted_count = db.query(ChatMessage).filter(*msg_filter).delete()
db.commit()
return {"deleted": True, "messages_deleted": deleted_count}
@router.get("/{agent_id}/sessions/{session_id}/export")
async def export_session(
agent_id: str,
session_id: str,
format: str = Query("json", pattern="^(json|markdown)$"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
workspace_id: str = Depends(get_current_workspace_id),
):
"""导出会话为 JSON 或 Markdown 格式"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
msg_query = db.query(ChatMessage).filter(
ChatMessage.agent_id == agent_id,
ChatMessage.session_id == session_id,
)
if workspace_id:
msg_query = msg_query.filter(ChatMessage.workspace_id == workspace_id)
messages = msg_query.order_by(ChatMessage.created_at.asc()).all()
if not messages:
raise HTTPException(status_code=404, detail="会话无消息")
# 获取会话标题
session = db.query(AgentSession).filter(AgentSession.id == session_id).first()
title = session.title if session and session.title else "对话"
if format == "markdown":
lines = [f"# {title}", "", f"**Agent**: {agent.name}", f"**时间**: {messages[0].created_at.isoformat() if messages[0].created_at else 'N/A'}", "", "---", ""]
for m in messages:
role_label = "用户" if m.role == "user" else "AI 助手" if m.role == "assistant" else m.role
lines.append(f"### {role_label}")
if m.content:
lines.append(m.content)
if m.tool_name:
lines.append(f"\n> 工具调用: `{m.tool_name}`")
if m.tool_output:
lines.append(f"\n<details><summary>工具输出</summary>\n\n```\n{m.tool_output[:500]}\n```\n</details>")
lines.append("")
return PlainTextResponse("\n".join(lines), media_type="text/markdown")
else:
# JSON
export_data = {
"title": title,
"agent_name": agent.name,
"agent_id": agent_id,
"session_id": session_id,
"exported_at": messages[0].created_at.isoformat() if messages[0].created_at else None,
"messages": [
{
"role": m.role,
"content": m.content,
"tool_name": m.tool_name,
"tool_input": m.tool_input,
"tool_output": m.tool_output,
"created_at": m.created_at.isoformat() if m.created_at else None,
}
for m in messages
],
}
return export_data