- Add company module (3-tier org, CEO planning, parallel departments) - Add company orchestrator, knowledge extractor, presets, scheduler - Add company API endpoints, models, and frontend views - Add 今天吃啥 PWA app (69 dishes, real images, offline support) - Add team_projects output directory structure - Add unified manage.ps1 for service lifecycle - Add Windows startup guide v1.0 - Add TTS troubleshooting doc - Update frontend (AgentChat UX overhaul, new views) - Update backend (voice engine fix, multi-tenant, RBAC) - Remove deprecated startup scripts and old docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
802 lines
29 KiB
Python
802 lines
29 KiB
Python
"""
|
||
虚拟公司 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="公司级项目目标")
|
||
|
||
|
||
# ─── 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}/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)
|
||
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):
|
||
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
|