"""项目路由:多类型项目 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}")