- 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>
181 lines
6.0 KiB
Python
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
|