BE-CAL-003: Add Agent model with status/heartbeat/exhausted fields
- New app/models/agent.py with Agent model, AgentStatus & ExhaustReason enums - Agent has 1-to-1 FK to User, unique agent_id (OpenClaw $AGENT_ID), claw_identifier (OpenClaw instance, convention-matches MonitoredServer.identifier) - Status fields: status (idle/on_call/busy/exhausted/offline), last_heartbeat - Exhausted tracking: exhausted_at, recovery_at, exhaust_reason (rate_limit/billing) - User model: added 'agent' back-reference (uselist=False) - Schemas: AgentResponse, AgentStatusUpdate, UserCreate now accepts agent_id+claw_identifier - UserResponse: includes agent_id when agent is bound - Users router: create_user creates Agent record when agent_id+claw_identifier provided - Auto-migration: CREATE TABLE agents in _migrate_schema() - Startup imports: agent and calendar models registered
This commit is contained in:
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_user, get_password_hash
|
||||
from app.core.config import get_db
|
||||
from app.models import models
|
||||
from app.models.agent import Agent
|
||||
from app.models.role_permission import Permission, Role, RolePermission
|
||||
from app.models.worklog import WorkLog
|
||||
from app.schemas import schemas
|
||||
@@ -17,6 +18,23 @@ from app.schemas import schemas
|
||||
router = APIRouter(prefix="/users", tags=["Users"])
|
||||
|
||||
|
||||
def _user_response(user: models.User) -> dict:
|
||||
"""Build a UserResponse-compatible dict that includes the agent_id when present."""
|
||||
data = {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"is_active": user.is_active,
|
||||
"is_admin": user.is_admin,
|
||||
"role_id": user.role_id,
|
||||
"role_name": user.role_name,
|
||||
"agent_id": user.agent.agent_id if user.agent else None,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
def require_admin(current_user: models.User = Depends(get_current_user)):
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin required")
|
||||
@@ -69,12 +87,27 @@ def create_user(
|
||||
db: Session = Depends(get_db),
|
||||
_: models.User = Depends(require_account_creator),
|
||||
):
|
||||
# Validate agent_id / claw_identifier: both or neither
|
||||
has_agent_id = bool(user.agent_id)
|
||||
has_claw = bool(user.claw_identifier)
|
||||
if has_agent_id != has_claw:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="agent_id and claw_identifier must both be provided or both omitted",
|
||||
)
|
||||
|
||||
existing = db.query(models.User).filter(
|
||||
(models.User.username == user.username) | (models.User.email == user.email)
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Username or email already exists")
|
||||
|
||||
# Check agent_id uniqueness
|
||||
if has_agent_id:
|
||||
existing_agent = db.query(Agent).filter(Agent.agent_id == user.agent_id).first()
|
||||
if existing_agent:
|
||||
raise HTTPException(status_code=400, detail="agent_id already in use")
|
||||
|
||||
assigned_role = _resolve_user_role(db, user.role_id)
|
||||
hashed_password = get_password_hash(user.password) if user.password else None
|
||||
db_user = models.User(
|
||||
@@ -87,9 +120,20 @@ def create_user(
|
||||
role_id=assigned_role.id,
|
||||
)
|
||||
db.add(db_user)
|
||||
db.flush() # get db_user.id
|
||||
|
||||
# Create Agent record if agent binding is requested (BE-CAL-003)
|
||||
if has_agent_id:
|
||||
db_agent = Agent(
|
||||
user_id=db_user.id,
|
||||
agent_id=user.agent_id,
|
||||
claw_identifier=user.claw_identifier,
|
||||
)
|
||||
db.add(db_agent)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
return _user_response(db_user)
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.UserResponse])
|
||||
@@ -99,7 +143,8 @@ def list_users(
|
||||
db: Session = Depends(get_db),
|
||||
_: models.User = Depends(require_admin),
|
||||
):
|
||||
return db.query(models.User).order_by(models.User.created_at.desc()).offset(skip).limit(limit).all()
|
||||
users = db.query(models.User).order_by(models.User.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return [_user_response(u) for u in users]
|
||||
|
||||
|
||||
def _find_user_by_id_or_username(db: Session, identifier: str) -> models.User | None:
|
||||
@@ -120,7 +165,7 @@ def get_user(
|
||||
user = _find_user_by_id_or_username(db, identifier)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
return _user_response(user)
|
||||
|
||||
|
||||
@router.patch("/{identifier}", response_model=schemas.UserResponse)
|
||||
@@ -159,7 +204,7 @@ def update_user(
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
return _user_response(user)
|
||||
|
||||
|
||||
@router.delete("/{identifier}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
Reference in New Issue
Block a user