Files
companion-assistant/backend/app/api/projects.py
T

178 lines
6.9 KiB
Python

"""项目路由:多类型项目 CRUD + 任务管理(任务执行走 agent)。"""
import json
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ..config import settings
from ..core.deps import get_current_user
from ..core.response import ok
from ..database import get_db
from ..models import Agent, Project, ProjectTask, User
from ..schemas.project import ProjectCreate, ProjectTaskCreate, ProjectUpdate
router = APIRouter(prefix="/projects", tags=["项目"])
PROJECT_TYPE_ICONS = {
"programming": "💻", "research": "🔬", "office": "💼", "writing": "📖", "video": "🎬",
}
def _project_out(p: Project) -> dict:
return {
"id": p.id, "name": p.name, "type": p.type, "description": p.description,
"status": p.status, "workspace_dir": p.workspace_dir, "settings": p.settings_dict(),
"icon": PROJECT_TYPE_ICONS.get(p.type, "📁"),
"created_at": p.created_at.isoformat(), "updated_at": p.updated_at.isoformat(),
}
def _task_out(t: ProjectTask) -> dict:
return {
"id": t.id, "project_id": t.project_id, "title": t.title, "description": t.description,
"status": t.status, "output": t.output,
"created_at": t.created_at.isoformat(),
"finished_at": t.finished_at.isoformat() if t.finished_at else None,
}
def _get_owned(db: Session, user: User, project_id: int) -> Project:
p = db.get(Project, project_id)
if not p or p.user_id != user.id:
raise HTTPException(status_code=404, detail="项目不存在")
return p
@router.get("")
def list_projects(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
projects = db.query(Project).filter(Project.user_id == user.id).order_by(Project.updated_at.desc()).all()
return ok([_project_out(p) for p in projects])
@router.post("")
def create_project(body: ProjectCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
p = Project(user_id=user.id, name=body.name, type=body.type, description=body.description,
settings=json.dumps(body.settings, ensure_ascii=False))
db.add(p)
db.commit()
db.refresh(p)
# 创建独立工作目录
ws = settings.WORKSPACE_DIR / f"project_{p.id}"
ws.mkdir(parents=True, exist_ok=True)
(ws / "docs").mkdir(exist_ok=True)
(ws / "output").mkdir(exist_ok=True)
p.workspace_dir = str(ws)
db.commit()
return ok(_project_out(p))
@router.get("/{project_id}")
def get_project(project_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
p = _get_owned(db, user, project_id)
tasks = db.query(ProjectTask).filter(ProjectTask.project_id == p.id).order_by(ProjectTask.id).all()
data = _project_out(p)
data["tasks"] = [_task_out(t) for t in tasks]
return ok(data)
@router.put("/{project_id}")
def update_project(project_id: int, body: ProjectUpdate,
user: User = Depends(get_current_user), db: Session = Depends(get_db)):
p = _get_owned(db, user, project_id)
data = body.model_dump(exclude_unset=True)
if "settings" in data:
data["settings"] = json.dumps(data["settings"], ensure_ascii=False)
for k, v in data.items():
setattr(p, k, v)
db.commit()
db.refresh(p)
return ok(_project_out(p))
@router.delete("/{project_id}")
def delete_project(project_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
p = _get_owned(db, user, project_id)
db.delete(p)
db.commit()
# 清理工作目录(保留,避免误删数据 —— 改名为 archive 前缀)
if p.workspace_dir and Path(p.workspace_dir).exists():
archived = Path(p.workspace_dir).parent / f"project_{p.id}_archived_{datetime.now().strftime('%Y%m%d%H%M%S')}"
Path(p.workspace_dir).rename(archived)
return ok(message="已删除")
# ---------- 项目任务 ----------
@router.get("/{project_id}/tasks")
def list_tasks(project_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
_get_owned(db, user, project_id)
tasks = db.query(ProjectTask).filter(ProjectTask.project_id == project_id).order_by(ProjectTask.id).all()
return ok([_task_out(t) for t in tasks])
@router.post("/{project_id}/tasks")
def create_task(project_id: int, body: ProjectTaskCreate,
user: User = Depends(get_current_user), db: Session = Depends(get_db)):
_get_owned(db, user, project_id)
t = ProjectTask(project_id=project_id, title=body.title, description=body.description, agent_id=body.agent_id)
db.add(t)
db.commit()
db.refresh(t)
return ok(_task_out(t))
@router.put("/{project_id}/tasks/{task_id}")
def update_task(project_id: int, task_id: int, body: dict,
user: User = Depends(get_current_user), db: Session = Depends(get_db)):
_get_owned(db, user, project_id)
t = db.get(ProjectTask, task_id)
if not t or t.project_id != project_id:
raise HTTPException(status_code=404, detail="任务不存在")
if "status" in body:
t.status = body["status"]
if body["status"] in ("done", "failed"):
t.finished_at = datetime.utcnow()
if "output" in body:
t.output = body["output"]
db.commit()
db.refresh(t)
return ok(_task_out(t))
@router.post("/{project_id}/tasks/{task_id}/run")
async def run_task(project_id: int, task_id: int,
user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""用智能体执行项目任务(非流式,适合项目产出)。"""
p = _get_owned(db, user, project_id)
t = db.get(ProjectTask, task_id)
if not t or t.project_id != project_id:
raise HTTPException(status_code=404, detail="任务不存在")
agent = db.get(Agent, t.agent_id) if t.agent_id else None
model = (agent.model if agent and agent.model else p.settings_dict().get("model", "")) or "deepseek-chat"
from ..core import llm
system = agent.system_prompt if agent else (
f"你是项目「{p.name}」(类型:{p.type})的执行助手。请围绕任务要求,输出完整、可直接使用的成果。"
)
prompt = f"项目:{p.name}\n任务:{t.title}\n要求:{t.description or '(无附加要求)'}\n\n请完成该任务并输出成果。"
t.status = "doing"
db.commit()
try:
output = await llm.chat_completion(
[{"role": "system", "content": system}, {"role": "user", "content": prompt}],
model=model, max_tokens=8192,
)
t.output = output
t.status = "done"
t.finished_at = datetime.utcnow()
db.commit()
return ok(_task_out(t))
except Exception as e:
t.status = "failed"
t.output = f"执行失败:{e}"
t.finished_at = datetime.utcnow()
db.commit()
raise HTTPException(status_code=502, detail=f"任务执行失败:{e}")