Files
aiagent/backend/app/api/companies.py
renjianbo eef48ce831 feat: CEO plan preview & confirm before company execution
- Add POST /companies/{id}/plan endpoint for stateless CEO plan preview (no DB write)
- execute()/execute_stream() accept optional confirmed ceo_plan to skip re-planning
- Frontend: plan preview dialog in CompanyBuilder to review/tweak analysis, dept goals & deliverables before running; previewCompanyPlan API with extended timeout
- Grant file_write tool to CEO/CTO presets; add echarts dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-12 00:40:22 +08:00

823 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
虚拟公司 API — 创建 / 管理 / 执行
"""
from __future__ import annotations
import json
import logging
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.auth import get_current_user
from app.core.database import get_db
from app.models.user import User
from app.models.agent import Agent
from app.models.company import Company, CompanyProject
from app.models.team import Team, TeamMember
from app.models.company_schedule import CompanySchedule
from app.models.company_knowledge import CompanyKnowledge
from app.services.company_orchestrator import CompanyOrchestrator
from app.services.company_presets import COMPANY_PRESETS
from app.services.company_knowledge_extractor import extract_from_project
from app.services.insight_analyzer import analyze_company
from app.services.scheduler import register_schedule, unregister_schedule
from app.services.project_supervisor import ProjectSupervisor
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/companies",
tags=["companies"],
responses={401: {"description": "未授权"}, 404: {"description": "资源不存在"}},
)
# ─── Schemas ───
class CompanyCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
industry: Optional[str] = None
workspace_id: Optional[str] = None
ceo_agent_id: Optional[str] = None
class CompanyUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = None
industry: Optional[str] = None
class DepartmentAdd(BaseModel):
team_id: str = Field(..., description="已有Team ID")
class ExecuteRequest(BaseModel):
project_description: str = Field(..., min_length=1, description="公司级项目目标")
ceo_plan: Optional[Dict[str, Any]] = Field(
None, description="已确认的 CEO 计划;提供时跳过二次规划,直接据此执行"
)
# ─── Endpoints ───
@router.get("")
def list_companies(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""列出用户的所有虚拟公司。"""
companies = (
db.query(Company)
.filter(Company.user_id == current_user.id, Company.status != "archived")
.order_by(Company.created_at.desc())
.all()
)
result = []
for c in companies:
dept_count = db.query(Team).filter(Team.parent_company_id == c.id).count()
d = c.to_dict()
d["department_count"] = dept_count
result.append(d)
return {"companies": result, "total": len(result)}
@router.get("/{company_id}")
def get_company(
company_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取公司详情(含部门列表)。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
departments = db.query(Team).filter(Team.parent_company_id == company_id).all()
return {
**company.to_dict(),
"departments": [d.to_dict(include_members=True) for d in departments],
}
@router.post("", status_code=201)
def create_company(
body: CompanyCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""手动创建虚拟公司。"""
company = Company(
id=str(uuid.uuid4()),
name=body.name,
description=body.description,
industry=body.industry,
workspace_id=body.workspace_id,
user_id=current_user.id,
ceo_agent_id=body.ceo_agent_id,
config={},
)
db.add(company)
db.commit()
return company.to_dict()
@router.post("/template/{preset_type}", status_code=201)
def create_company_from_preset(
preset_type: str,
workspace_id: Optional[str] = Query(None),
company_name: Optional[str] = Query(None, description="自定义公司名称,覆盖预设默认名"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""从行业预设一键创建虚拟公司(含部门 + 团队)。
基于 company_presets.py 的角色定义,自动创建公司 + 多个部门 + Agent + 填充成员。
可通过 company_name 自定义公司名称。
"""
preset = COMPANY_PRESETS.get(preset_type)
if not preset:
raise HTTPException(status_code=400, detail=f"未知预设类型: {preset_type}")
# 1. 创建公司 (支持自定义名称)
company_name_final = company_name.strip() if company_name else preset["name"]
company = Company(
id=str(uuid.uuid4()),
name=company_name_final,
description=preset["description"],
industry=preset["id"],
workspace_id=workspace_id,
user_id=current_user.id,
config={"preset_type": preset_type},
)
db.add(company)
db.flush()
# 2. 按分组创建部门和 Agent
groups: Dict[str, List[Dict]] = {}
for role in preset["roles"]:
g = role["group"]
if g not in groups:
groups[g] = []
groups[g].append(role)
created_departments = []
for group_name, roles in groups.items():
# 找到 leader第一个角色
leader = roles[0]
# 创建部门的 Agent复用或新建
dept_agents = []
for rc in roles:
existing = (
db.query(Agent)
.filter(Agent.name == rc["name"], Agent.user_id == current_user.id)
.first()
)
if existing:
dept_agents.append(existing)
continue
agent = Agent(
id=str(uuid.uuid4()),
name=rc["name"],
description=rc["description"],
agent_type="specialist",
user_id=current_user.id,
workspace_id=workspace_id,
workflow_config={
"nodes": [
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 120}, "data": {}},
{
"id": "llm-1",
"type": "llm",
"position": {"x": 320, "y": 120},
"data": {
"prompt": rc["system_prompt"],
"temperature": rc["temperature"],
"model": rc["model"],
"provider": "deepseek",
"enable_tools": True,
"tools": rc["tools"],
"selected_tools": rc["tools"],
"max_iterations": rc["max_iterations"],
},
},
{"id": "end-1", "type": "end", "position": {"x": 560, "y": 120}, "data": {}},
],
"edges": [
{"id": "e1", "source": "start-1", "target": "llm-1", "sourceHandle": "right", "targetHandle": "left"},
{"id": "e2", "source": "llm-1", "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
],
},
status="published",
category="company_department",
tags=[rc["role"], preset_type, "company_department"],
)
db.add(agent)
db.flush()
dept_agents.append(agent)
# 创建部门Team
dept = Team(
id=str(uuid.uuid4()),
name=f"{company_name_final}-{group_name}",
description=f"{company_name_final} {group_name}部门",
user_id=current_user.id,
workspace_id=workspace_id,
department_type=_map_group_to_dept_type(group_name),
parent_company_id=company.id,
config={"workflow": preset_type, "department": True},
)
db.add(dept)
db.flush()
# 添加成员
for i, ag in enumerate(dept_agents):
member = TeamMember(
id=str(uuid.uuid4()),
team_id=dept.id,
agent_id=ag.id,
role=roles[i]["role"] if i < len(roles) else "member",
position=i,
is_lead=(i == 0),
)
db.add(member)
created_departments.append({
"id": dept.id,
"name": dept.name,
"department_type": dept.department_type,
"members": [{"agent_id": ag.id, "name": ag.name, "role": roles[j]["role"] if j < len(roles) else "member", "is_lead": j == 0} for j, ag in enumerate(dept_agents)],
})
# 3. 设置 CEO第一个部门的 leader 作为CEO或查已有CEO Agent
ceo_candidate = (
db.query(Agent)
.filter(Agent.name.like("%CEO%"), Agent.user_id == current_user.id)
.first()
)
if not ceo_candidate and created_departments and created_departments[0]["members"]:
ceo_candidate = db.query(Agent).filter(Agent.id == created_departments[0]["members"][0]["agent_id"]).first()
if ceo_candidate:
company.ceo_agent_id = ceo_candidate.id
db.commit()
db.refresh(company)
logger.info(
"从预设创建虚拟公司: preset=%s company=%s departments=%d",
preset_type, company.id, len(created_departments),
)
return {
**company.to_dict(),
"departments": created_departments,
"total_agents": sum(len(d["members"]) for d in created_departments),
}
@router.put("/{company_id}")
def update_company(
company_id: str,
body: CompanyUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""更新公司信息(名称、描述、行业)。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
if company.user_id != current_user.id:
raise HTTPException(status_code=403, detail="无权操作")
if body.name is not None:
company.name = body.name
if body.description is not None:
company.description = body.description
if body.industry is not None:
company.industry = body.industry
db.commit()
db.refresh(company)
return company.to_dict()
@router.delete("/{company_id}")
def delete_company(
company_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""归档公司(软删除)。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
if company.user_id != current_user.id:
raise HTTPException(status_code=403, detail="无权操作")
company.status = "archived"
db.commit()
return {"message": "公司已归档"}
# ─── 部门管理 ───
@router.post("/{company_id}/departments", status_code=201)
def add_department(
company_id: str,
body: DepartmentAdd,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""将已有团队添加为公司部门。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
team = db.query(Team).filter(Team.id == body.team_id, Team.user_id == current_user.id).first()
if not team:
raise HTTPException(status_code=404, detail="团队不存在")
if team.parent_company_id:
raise HTTPException(status_code=400, detail="该团队已属于其他公司")
team.parent_company_id = company_id
db.commit()
return team.to_dict(include_members=True)
@router.delete("/{company_id}/departments/{department_id}")
def remove_department(
company_id: str,
department_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""从公司移除部门(不解散团队)。"""
team = db.query(Team).filter(Team.id == department_id, Team.parent_company_id == company_id).first()
if not team:
raise HTTPException(status_code=404, detail="部门不存在")
team.parent_company_id = None
db.commit()
return {"message": "部门已移除"}
# ─── 项目执行 ───
@router.post("/{company_id}/plan")
async def plan_company(
company_id: str,
body: ExecuteRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""仅生成 CEO 计划供预览/确认,不派发部门、不落库。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
orchestrator = CompanyOrchestrator(db, company_id, current_user.id)
try:
return await orchestrator.preview_plan(body.project_description)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{company_id}/execute")
async def execute_company(
company_id: str,
body: ExecuteRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""执行公司级项目(非流式)。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
orchestrator = CompanyOrchestrator(db, company_id, current_user.id)
try:
result = await orchestrator.execute(body.project_description, ceo_plan=body.ceo_plan)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{company_id}/execute/stream")
async def execute_company_stream(
company_id: str,
body: ExecuteRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""执行公司级项目SSE 流式)。"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
orchestrator = CompanyOrchestrator(db, company_id, current_user.id)
async def event_stream():
try:
async for event in orchestrator.execute_stream(body.project_description, ceo_plan=body.ceo_plan):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
return StreamingResponse(event_stream(), media_type="text/event-stream")
# ─── 统计 ───
@router.get("/{company_id}/stats")
def get_company_stats(
company_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取公司统计信息(项目数、成功率、最近活跃时间)。"""
projects = (
db.query(CompanyProject)
.filter(CompanyProject.company_id == company_id)
.order_by(CompanyProject.created_at.desc())
.all()
)
total = len(projects)
completed = sum(1 for p in projects if p.status == "completed")
success_rate = round(completed / total * 100, 1) if total > 0 else 0
last_active = projects[0].created_at.isoformat() if projects else None
return {
"total_projects": total,
"completed_projects": completed,
"success_rate": success_rate,
"last_active_at": last_active,
}
# ─── 导出 ───
@router.get("/{company_id}/export")
def export_company(
company_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""导出公司架构 + 所有项目结果为 Markdown 文件。"""
from fastapi.responses import Response
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(status_code=404, detail="公司不存在")
departments = db.query(Team).filter(Team.parent_company_id == company_id).all()
projects = (
db.query(CompanyProject)
.filter(CompanyProject.company_id == company_id)
.order_by(CompanyProject.created_at.desc())
.all()
)
lines = [
f"# {company.name}",
"",
f"- **行业**: {company.industry or '通用'}",
f"- **描述**: {company.description or ''}",
f"- **创建时间**: {company.created_at.isoformat() if company.created_at else '未知'}",
f"- **状态**: {company.status}",
"",
"---",
"",
"## 组织架构",
"",
]
for i, dept in enumerate(departments, 1):
lines.append(f"### {i}. {dept.name}")
lines.append(f"- **类型**: {dept.department_type or '通用'}")
if dept.members:
lines.append("- **成员**:")
for m in dept.members:
agent_name = m.agent.name if hasattr(m, 'agent') and m.agent else m.agent_id
lines.append(f" - {agent_name} ({m.role})" + (" [Leader]" if m.is_lead else ""))
lines.append("")
lines.extend([
"---",
"",
f"## 项目历史 ({len(projects)} 个项目)",
"",
])
for i, proj in enumerate(projects, 1):
lines.append(f"### {i}. {proj.name}")
lines.append(f"- **状态**: {proj.status}")
lines.append(f"- **创建时间**: {proj.created_at.isoformat() if proj.created_at else '未知'}")
if proj.completed_at:
lines.append(f"- **完成时间**: {proj.completed_at.isoformat()}")
if proj.ceo_plan:
lines.append(f"- **CEO 规划**:")
analysis = proj.ceo_plan.get("analysis", "") if isinstance(proj.ceo_plan, dict) else ""
if analysis:
lines.append(f" {analysis}")
dept_plans = proj.ceo_plan.get("departments", []) if isinstance(proj.ceo_plan, dict) else []
if dept_plans:
lines.append(" - **部门任务**:")
for dp in dept_plans:
lines.append(f" - {dp.get('department_name', '未知')}: {dp.get('goal', '')}")
lines.append("")
markdown = "\n".join(lines)
return Response(
content=markdown,
media_type="text/markdown; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{company.name}_export.md"'},
)
# ─── 项目历史 ───
@router.get("/{company_id}/projects")
def list_projects(
company_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""列出公司的项目执行历史。"""
projects = (
db.query(CompanyProject)
.filter(CompanyProject.company_id == company_id)
.order_by(CompanyProject.created_at.desc())
.all()
)
return {"projects": [p.to_dict() for p in projects]}
def _map_group_to_dept_type(group_name: str) -> str:
"""将角色分组名映射到部门类型。"""
mapping = {
"战略层": "executive",
"产品层": "product",
"交付层": "engineering",
"运营层": "operations",
"服务层": "sales",
"业务层": "marketing",
"供应链": "operations",
"支持层": "hr",
"执行层": "marketing",
"创意层": "creative",
"制作层": "production",
}
return mapping.get(group_name, "operations")
# ─── 定时调度 ───
class ScheduleCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
project_goal: str = Field(..., min_length=1)
cron_expression: Optional[str] = None
interval_minutes: Optional[str] = None
enabled: bool = True
class ScheduleUpdate(BaseModel):
name: Optional[str] = None
project_goal: Optional[str] = None
cron_expression: Optional[str] = None
interval_minutes: Optional[str] = None
enabled: Optional[bool] = None
@router.get("/{company_id}/schedules")
def list_schedules(company_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""列出公司的定时调度。"""
schedules = db.query(CompanySchedule).filter(CompanySchedule.company_id == company_id).order_by(CompanySchedule.created_at.desc()).all()
return {"schedules": [s.to_dict() for s in schedules]}
@router.post("/{company_id}/schedules", status_code=201)
def create_schedule(company_id: str, body: ScheduleCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""创建定时调度。"""
import uuid
cs = CompanySchedule(
id=str(uuid.uuid4()),
company_id=company_id,
name=body.name,
project_goal=body.project_goal,
cron_expression=body.cron_expression,
interval_minutes=body.interval_minutes,
enabled=body.enabled,
)
db.add(cs)
db.commit()
if cs.enabled and cs.cron_expression:
try:
register_schedule(cs)
db.commit()
except Exception:
pass
return cs.to_dict()
@router.put("/schedules/{schedule_id}")
def update_schedule(schedule_id: str, body: ScheduleUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""更新调度。"""
cs = db.query(CompanySchedule).filter(CompanySchedule.id == schedule_id).first()
if not cs:
raise HTTPException(status_code=404, detail="调度不存在")
if body.name is not None:
cs.name = body.name
if body.project_goal is not None:
cs.project_goal = body.project_goal
if body.cron_expression is not None:
cs.cron_expression = body.cron_expression
if body.interval_minutes is not None:
cs.interval_minutes = body.interval_minutes
if body.enabled is not None:
cs.enabled = body.enabled
db.commit()
if cs.enabled and cs.cron_expression:
try:
register_schedule(cs)
db.commit()
except Exception:
pass
else:
unregister_schedule(schedule_id)
return cs.to_dict()
@router.delete("/schedules/{schedule_id}")
def delete_schedule(schedule_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""删除调度。"""
cs = db.query(CompanySchedule).filter(CompanySchedule.id == schedule_id).first()
if not cs:
raise HTTPException(status_code=404, detail="调度不存在")
unregister_schedule(schedule_id)
db.delete(cs)
db.commit()
return {"message": "调度已删除"}
@router.post("/schedules/{schedule_id}/trigger")
async def trigger_schedule(schedule_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""手动触发调度。"""
from app.services.scheduler import _execute_scheduled_project
cs = db.query(CompanySchedule).filter(CompanySchedule.id == schedule_id).first()
if not cs:
raise HTTPException(status_code=404, detail="调度不存在")
try:
await _execute_scheduled_project(schedule_id)
return {"message": "调度已触发"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ─── 知识库 ───
@router.post("/{company_id}/knowledge/generate", status_code=201)
def generate_knowledge(company_id: str, project_id: str = Query(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""从指定项目生成知识条目。"""
project = db.query(CompanyProject).filter(CompanyProject.id == project_id, CompanyProject.company_id == company_id).first()
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
entries = extract_from_project(project, db)
db.commit()
return {"message": f"已生成 {len(entries)} 条知识", "entries": [e.to_dict() for e in entries]}
@router.get("/{company_id}/knowledge")
def list_knowledge(company_id: str, search: Optional[str] = Query(None), category: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""列出公司知识库。"""
q = db.query(CompanyKnowledge).filter(CompanyKnowledge.company_id == company_id)
if search:
q = q.filter(CompanyKnowledge.title.contains(search) | CompanyKnowledge.content.contains(search))
if category:
q = q.filter(CompanyKnowledge.category == category)
entries = q.order_by(CompanyKnowledge.created_at.desc()).all()
return {"knowledge": [e.to_dict() for e in entries], "total": len(entries)}
@router.get("/{company_id}/knowledge/{knowledge_id}")
def get_knowledge(company_id: str, knowledge_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
entry = db.query(CompanyKnowledge).filter(CompanyKnowledge.id == knowledge_id, CompanyKnowledge.company_id == company_id).first()
if not entry:
raise HTTPException(status_code=404, detail="知识条目不存在")
return entry.to_dict()
@router.delete("/{company_id}/knowledge/{knowledge_id}")
def delete_knowledge(company_id: str, knowledge_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
entry = db.query(CompanyKnowledge).filter(CompanyKnowledge.id == knowledge_id, CompanyKnowledge.company_id == company_id).first()
if not entry:
raise HTTPException(status_code=404, detail="知识条目不存在")
db.delete(entry)
db.commit()
return {"message": "知识条目已删除"}
# ─── 洞察分析 ───
@router.get("/{company_id}/insights")
def get_insights(company_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""获取公司跨项目洞察分析。"""
result = analyze_company(company_id, db)
return result
# ─── 项目模板市场 ───
@router.get("/templates/list")
def list_public_templates(search: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""列出所有已发布的公开项目模板。"""
projects = db.query(CompanyProject).filter(CompanyProject.status == "completed").all()
templates = []
for p in projects:
cfg = p.config or {}
if cfg.get("is_public"):
if search and search not in (p.name or "") and search not in (p.description or ""):
continue
templates.append({
"id": p.id,
"company_id": p.company_id,
"name": p.name,
"description": p.description,
"ceo_plan_structure": {
"departments": [{"department_name": d.get("department_name", ""), "department_type": d.get("department_type", ""), "goal": d.get("goal", "")} for d in (p.ceo_plan or {}).get("departments", [])]
},
"created_at": p.created_at.isoformat() if p.created_at else None,
})
return {"templates": templates, "total": len(templates)}
@router.post("/{company_id}/projects/{project_id}/publish")
def publish_project(company_id: str, project_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""将项目发布为公开模板。"""
project = db.query(CompanyProject).filter(CompanyProject.id == project_id, CompanyProject.company_id == company_id).first()
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
cfg = project.config or {}
cfg["is_public"] = True
project.config = cfg
db.commit()
return {"message": "项目已发布为公开模板"}
@router.post("/{company_id}/projects/{project_id}/unpublish")
def unpublish_project(company_id: str, project_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""取消公开模板发布。"""
project = db.query(CompanyProject).filter(CompanyProject.id == project_id, CompanyProject.company_id == company_id).first()
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
cfg = project.config or {}
cfg["is_public"] = False
project.config = cfg
db.commit()
return {"message": "已取消公开"}
# ─── Project Supervisor / 项目监管 API ───
@router.get("/supervisor/zombies")
def get_zombie_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""获取所有僵尸项目(跨所有公司)。"""
sup = ProjectSupervisor(db)
return sup.get_zombie_summary()
@router.get("/supervisor/in-progress")
def get_in_progress_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""获取所有运行中的项目及其风险状态。"""
sup = ProjectSupervisor(db)
return {"projects": sup.get_all_in_progress()}
@router.post("/supervisor/scan")
def trigger_supervisor_scan(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""手动触发一次僵尸项目扫描。"""
sup = ProjectSupervisor(db)
result = sup.auto_detect_and_mark()
return result
@router.post("/{company_id}/projects/{project_id}/recover")
def recover_project(company_id: str, project_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""恢复僵尸项目(标记为 failed 以便重新执行)。"""
project = db.query(CompanyProject).filter(CompanyProject.id == project_id, CompanyProject.company_id == company_id).first()
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
sup = ProjectSupervisor(db)
result = sup.recover_project(project_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return result