feat: add Goal/Task data models, service layer, and API routes (Phase 1)

Main Agent 数字员工工厂基础设施:
- 新增 Goal 和 Task SQLAlchemy 数据模型
- Agent 模型新增 agent_type / input_schema / output_schema
- Execution 模型新增 goal_id 关联
- 新增 Goal/Task CRUD 服务层(含依赖检查、任务树、进度计算)
- 新增 /api/v1/goals (9端点) + /api/v1/tasks (8端点)
- 数据库迁移 013_add_goals_tasks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-08 19:50:16 +08:00
parent 10ee7ee625
commit 02d7cf8f62
11 changed files with 1017 additions and 2 deletions

204
backend/app/api/goals.py Normal file
View File

@@ -0,0 +1,204 @@
"""
Goal API — 目标管理接口
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
import logging
from app.core.database import get_db
from app.api.auth import get_current_user
from app.models.user import User
from app.services import goal_service
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/goals",
tags=["goals"],
responses={
401: {"description": "未授权"},
404: {"description": "资源不存在"},
400: {"description": "请求参数错误"},
},
)
# ──────────────────────────── Schemas ────────────────────────────
class GoalCreate(BaseModel):
title: str
description: str = ""
priority: int = Field(default=5, ge=1, le=10)
deadline: Optional[datetime] = None
main_agent_id: Optional[str] = None
parent_goal_id: Optional[str] = None
autonomy_config: Optional[Dict[str, Any]] = None
class GoalUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
priority: Optional[int] = Field(default=None, ge=1, le=10)
plan: Optional[List[Dict[str, Any]]] = None
autonomy_config: Optional[Dict[str, Any]] = None
deadline: Optional[datetime] = None
main_agent_id: Optional[str] = None
class GoalResponse(BaseModel):
id: str
title: str
description: Optional[str]
status: str
priority: int
progress: float
plan: Optional[Any]
autonomy_config: Optional[Any]
creator_id: str
main_agent_id: Optional[str]
parent_goal_id: Optional[str]
started_at: Optional[datetime]
completed_at: Optional[datetime]
deadline: Optional[datetime]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class GoalTaskTreeResponse(BaseModel):
goal: Dict[str, Any]
tasks: List[Dict[str, Any]]
# ──────────────────────────── Endpoints ────────────────────────────
@router.post("", response_model=GoalResponse, status_code=201)
def create_goal(
data: GoalCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""创建新目标"""
return goal_service.create_goal(
db=db,
creator_id=current_user.id,
title=data.title,
description=data.description,
priority=data.priority,
deadline=data.deadline,
main_agent_id=data.main_agent_id,
parent_goal_id=data.parent_goal_id,
autonomy_config=data.autonomy_config,
)
@router.get("", response_model=List[GoalResponse])
def list_goals(
status: Optional[str] = None,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""列出目标"""
return goal_service.list_goals(
db=db,
creator_id=current_user.id,
status=status,
skip=skip,
limit=limit,
)
@router.get("/{goal_id}", response_model=GoalResponse)
def get_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""获取目标详情"""
return goal_service.get_goal(db, goal_id)
@router.put("/{goal_id}", response_model=GoalResponse)
def update_goal(
goal_id: str,
data: GoalUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""更新目标"""
return goal_service.update_goal(
db=db,
goal_id=goal_id,
title=data.title,
description=data.description,
status=data.status,
priority=data.priority,
plan=data.plan,
autonomy_config=data.autonomy_config,
deadline=data.deadline,
main_agent_id=data.main_agent_id,
)
@router.delete("/{goal_id}", status_code=204)
def delete_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""删除目标"""
goal_service.delete_goal(db, goal_id)
return None
@router.post("/{goal_id}/start", response_model=GoalResponse)
def start_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""启动目标执行"""
goal = goal_service.update_goal(db, goal_id, status="active")
goal.started_at = datetime.now()
db.commit()
db.refresh(goal)
logger.info(f"Goal started: {goal_id}")
return goal
@router.post("/{goal_id}/pause", response_model=GoalResponse)
def pause_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""暂停目标执行"""
return goal_service.update_goal(db, goal_id, status="paused")
@router.post("/{goal_id}/resume", response_model=GoalResponse)
def resume_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""恢复目标执行"""
return goal_service.update_goal(db, goal_id, status="active")
@router.get("/{goal_id}/tasks", response_model=GoalTaskTreeResponse)
def get_goal_task_tree(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""获取目标的任务树"""
return goal_service.get_goal_task_tree(db, goal_id)

240
backend/app/api/tasks.py Normal file
View File

@@ -0,0 +1,240 @@
"""
Task API — 任务管理接口
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
import logging
from app.core.database import get_db
from app.api.auth import get_current_user
from app.models.user import User
from app.services import goal_service
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/tasks",
tags=["tasks"],
responses={
401: {"description": "未授权"},
404: {"description": "资源不存在"},
400: {"description": "请求参数错误"},
},
)
# ──────────────────────────── Schemas ────────────────────────────
class TaskCreate(BaseModel):
goal_id: str
title: str
description: str = ""
priority: int = Field(default=5, ge=1, le=10)
parent_task_id: Optional[str] = None
depends_on: Optional[List[str]] = None
assigned_agent_id: Optional[str] = None
assigned_agent_name: Optional[str] = None
task_config: Optional[Dict[str, Any]] = None
deadline: Optional[datetime] = None
requires_approval: bool = False
approver_id: Optional[str] = None
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
priority: Optional[int] = Field(default=None, ge=1, le=10)
task_config: Optional[Dict[str, Any]] = None
depends_on: Optional[List[str]] = None
assigned_agent_id: Optional[str] = None
assigned_agent_name: Optional[str] = None
result: Optional[Dict[str, Any]] = None
error_message: Optional[str] = None
execution_id: Optional[str] = None
deadline: Optional[datetime] = None
class TaskResponse(BaseModel):
id: str
goal_id: str
title: str
description: Optional[str]
status: str
priority: int
task_config: Optional[Any]
parent_task_id: Optional[str]
depends_on: Optional[Any]
result: Optional[Any]
error_message: Optional[str]
execution_id: Optional[str]
assigned_agent_id: Optional[str]
assigned_agent_name: Optional[str]
requires_approval: bool
approver_id: Optional[str]
approval_status: Optional[str]
started_at: Optional[datetime]
completed_at: Optional[datetime]
deadline: Optional[datetime]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class TaskDependencyCheck(BaseModel):
task_id: str
dependencies_met: bool
pending_dependencies: List[str] = []
# ──────────────────────────── Endpoints ────────────────────────────
@router.post("", response_model=TaskResponse, status_code=201)
def create_task(
data: TaskCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""创建新任务"""
return goal_service.create_task(
db=db,
goal_id=data.goal_id,
title=data.title,
description=data.description,
priority=data.priority,
parent_task_id=data.parent_task_id,
depends_on=data.depends_on,
assigned_agent_id=data.assigned_agent_id,
assigned_agent_name=data.assigned_agent_name,
task_config=data.task_config,
deadline=data.deadline,
requires_approval=data.requires_approval,
approver_id=data.approver_id,
)
@router.get("", response_model=List[TaskResponse])
def list_tasks(
goal_id: Optional[str] = None,
status: Optional[str] = None,
assigned_agent_id: Optional[str] = None,
parent_task_id: Optional[str] = None,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=50, ge=1, le=200),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""列出任务"""
return goal_service.list_tasks(
db=db,
goal_id=goal_id,
status=status,
assigned_agent_id=assigned_agent_id,
parent_task_id=parent_task_id,
skip=skip,
limit=limit,
)
@router.get("/{task_id}", response_model=TaskResponse)
def get_task(
task_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""获取任务详情"""
return goal_service.get_task(db, task_id)
@router.put("/{task_id}", response_model=TaskResponse)
def update_task(
task_id: str,
data: TaskUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""更新任务"""
return goal_service.update_task(
db=db,
task_id=task_id,
title=data.title,
description=data.description,
status=data.status,
priority=data.priority,
task_config=data.task_config,
depends_on=data.depends_on,
assigned_agent_id=data.assigned_agent_id,
assigned_agent_name=data.assigned_agent_name,
result=data.result,
error_message=data.error_message,
execution_id=data.execution_id,
deadline=data.deadline,
)
@router.delete("/{task_id}", status_code=204)
def delete_task(
task_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""删除任务"""
goal_service.delete_task(db, task_id)
return None
@router.get("/{task_id}/check-dependencies", response_model=TaskDependencyCheck)
def check_task_dependencies(
task_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""检查任务的前置依赖是否满足"""
met = goal_service.get_task_dependencies_met(db, task_id)
task = goal_service.get_task(db, task_id)
pending = []
if not met:
for dep_id in (task.depends_on or []):
from app.models.task import Task
dep = db.query(Task).filter(Task.id == dep_id).first()
if dep and dep.status != "completed":
pending.append(dep_id)
return TaskDependencyCheck(
task_id=task_id,
dependencies_met=met,
pending_dependencies=pending,
)
@router.post("/{task_id}/approve", response_model=TaskResponse)
def approve_task(
task_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""审批通过任务"""
return goal_service.update_task(
db=db,
task_id=task_id,
status="in_progress",
)
@router.post("/{task_id}/reject", response_model=TaskResponse)
def reject_task(
task_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""审批驳回任务"""
return goal_service.update_task(
db=db,
task_id=task_id,
status="failed",
error_message="审批驳回",
)