1500 lines
46 KiB
Python
1500 lines
46 KiB
Python
"""
|
||
AI Steward API - 主动式 AI 管家接口
|
||
|
||
核心功能:
|
||
1. 零配置项目创建
|
||
2. 主动信号捕获
|
||
3. 认知冲突检测
|
||
4. 阻塞自动解决
|
||
5. 个性化进度简报
|
||
"""
|
||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||
from sqlalchemy import and_, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from pydantic import BaseModel, Field
|
||
from typing import List, Optional, Dict, Any, Literal
|
||
from datetime import datetime, timezone, timedelta
|
||
import json
|
||
|
||
from ..core.response import success_response
|
||
from ..database import get_db
|
||
from ..dependencies.auth import get_current_user
|
||
from ..dependencies.permissions import require_manager_user
|
||
from ..dependencies.tenant import get_tenant_id
|
||
from ..models import ProjectSignalLink, Signal, User
|
||
from ..services.ai_steward import get_ai_steward_service, AIStewardService
|
||
from ..services.signal_capture import ProactiveSignalCaptureService
|
||
from ..services.cognitive_conflict import get_cognitive_conflict_service
|
||
from ..services.blocker_detection import get_blocker_detection_service
|
||
from ..services.daily_briefing import get_daily_briefing_service
|
||
from ..services.event_workflow import get_event_workflow_engine, WorkflowEventType
|
||
from ..services.adaptive_rag_service import AdaptiveRAGService
|
||
from ..services.ai_orchestration_run_service import AIOrchestrationRunService
|
||
from ..services.feishu_outbox_service import FeishuOutboxService
|
||
from ..services.openclaw_commander_service import OpenClawCommanderService
|
||
from ..services.signal_source_orchestration_service import SignalSourceOrchestrationService
|
||
from ..services.signal_source_repair_service import (
|
||
SignalSourceRepairMissingSignalError,
|
||
SignalSourceRepairService,
|
||
SignalSourceRepairTimeoutError,
|
||
)
|
||
from ..schemas.rag import RAGAskRequest
|
||
from ..utils.audited_write import audited_write
|
||
|
||
|
||
from ..utils.timezone import now_cn_aware, now_cn_naive
|
||
router = APIRouter(prefix="/ai-steward", tags=["AI Steward"])
|
||
|
||
|
||
def _utc_now_naive() -> datetime:
|
||
return now_cn_naive()
|
||
|
||
|
||
async def _ensure_project_signal_link(
|
||
db: AsyncSession,
|
||
*,
|
||
tenant_id: int,
|
||
project_id: int,
|
||
signal_id: int,
|
||
link_type: str = "related",
|
||
) -> None:
|
||
existing = (
|
||
await db.execute(
|
||
select(ProjectSignalLink.id).where(
|
||
and_(
|
||
ProjectSignalLink.tenant_id == int(tenant_id),
|
||
ProjectSignalLink.project_id == int(project_id),
|
||
ProjectSignalLink.signal_id == int(signal_id),
|
||
)
|
||
).limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
if existing is not None:
|
||
return
|
||
db.add(
|
||
ProjectSignalLink(
|
||
tenant_id=int(tenant_id),
|
||
project_id=int(project_id),
|
||
signal_id=int(signal_id),
|
||
link_type=link_type,
|
||
relevance_score=1.0,
|
||
)
|
||
)
|
||
|
||
|
||
# ==================== Request/Response Models ====================
|
||
|
||
class ProjectFromTextRequest(BaseModel):
|
||
"""零配置项目创建请求"""
|
||
input_text: str = Field(
|
||
...,
|
||
description="自然语言项目描述,例如:我们要做一个 AI 客服工具,基于 GPT-4o mini,对标 XX 竞品,3 周上线",
|
||
min_length=10,
|
||
max_length=500
|
||
)
|
||
|
||
|
||
class ProjectFromTextResponse(BaseModel):
|
||
"""零配置项目创建响应"""
|
||
project: Dict[str, Any]
|
||
signal: Dict[str, Any]
|
||
charter: Dict[str, Any]
|
||
tasks: List[Dict[str, Any]]
|
||
risks: List[Dict[str, Any]]
|
||
analysis: Dict[str, Any]
|
||
|
||
|
||
class CognitiveConflictResponse(BaseModel):
|
||
"""认知冲突响应"""
|
||
id: str
|
||
type: str
|
||
description: str
|
||
severity: str
|
||
resolution_suggestion: Optional[str] = None
|
||
related_users: List[str]
|
||
|
||
|
||
class DailyBriefingResponse(BaseModel):
|
||
"""每日简报响应"""
|
||
date: str
|
||
total_updates: int
|
||
your_updates: int
|
||
relevant_changes: List[Dict[str, Any]]
|
||
risk_alerts: List[Dict[str, Any]]
|
||
alignment_needed: List[Dict[str, Any]]
|
||
|
||
|
||
class FeishuCaptureRequest(BaseModel):
|
||
"""飞书信号捕获请求"""
|
||
group_chat_id: str
|
||
message_content: str
|
||
sender: str
|
||
feishu_message_id: Optional[str] = None
|
||
|
||
|
||
class BrowserCaptureRequest(BaseModel):
|
||
"""浏览器信号捕获请求"""
|
||
user_id: int
|
||
url: str
|
||
title: str
|
||
content_preview: str
|
||
|
||
|
||
class GitHubCaptureRequest(BaseModel):
|
||
"""GitHub信号捕获请求"""
|
||
repo_url: str
|
||
event_type: str
|
||
event_data: Dict[str, Any]
|
||
|
||
|
||
class ConflictDetectRequest(BaseModel):
|
||
"""认知冲突检测请求"""
|
||
project_id: int
|
||
detect_type: str = "all" # all, charter, definition
|
||
|
||
|
||
class OpenClawCommandDispatchRequest(BaseModel):
|
||
"""OpenClaw 指挥层任务分配请求"""
|
||
project_id: int = Field(..., ge=1)
|
||
instruction: str = Field(..., min_length=4, max_length=2000)
|
||
dry_run: bool = False
|
||
max_operations: int = Field(default=8, ge=1, le=20)
|
||
response_mode: Literal["fast", "complete"] = "fast"
|
||
|
||
|
||
class SignalSourceOnboardRequest(BaseModel):
|
||
project_id: int = Field(..., ge=1)
|
||
instruction: str = Field(..., min_length=6, max_length=2000)
|
||
dry_run: bool = False
|
||
response_mode: Literal["fast", "complete"] = "fast"
|
||
|
||
|
||
class SignalSourceListResponse(BaseModel):
|
||
items: List[Dict[str, Any]]
|
||
|
||
|
||
class SignalSourceRepairRequest(BaseModel):
|
||
project_id: int = Field(..., ge=1)
|
||
registry_ids: List[int] = Field(default_factory=list, min_length=1, max_length=50)
|
||
sender: str = Field(default="PMFlow修复器", min_length=1, max_length=64)
|
||
|
||
|
||
def _parse_related_users(raw_value: Any) -> List[str]:
|
||
"""兼容 involved_user_ids 的 list / json-string / 单值字符串。"""
|
||
if raw_value is None:
|
||
return []
|
||
if isinstance(raw_value, list):
|
||
return [str(item) for item in raw_value if item is not None]
|
||
if isinstance(raw_value, (bytes, bytearray)):
|
||
raw_value = raw_value.decode("utf-8", errors="ignore")
|
||
if isinstance(raw_value, str):
|
||
text = raw_value.strip()
|
||
if not text:
|
||
return []
|
||
try:
|
||
parsed = json.loads(text)
|
||
except (TypeError, ValueError, json.JSONDecodeError):
|
||
if "," in text:
|
||
return [v.strip() for v in text.split(",") if v.strip()]
|
||
return [text]
|
||
if isinstance(parsed, list):
|
||
return [str(item) for item in parsed if item is not None]
|
||
if parsed is None:
|
||
return []
|
||
return [str(parsed)]
|
||
return [str(raw_value)]
|
||
|
||
|
||
# ==================== Zero-Config Project Creation ====================
|
||
|
||
@router.post("/rag/ask")
|
||
async def ask_ai_steward_rag(
|
||
request: RAGAskRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
||
service = AdaptiveRAGService(db, tenant_id=effective_tenant_id)
|
||
result = await service.answer_question(
|
||
question=request.question,
|
||
asked_by=int(current_user.id),
|
||
asked_via="pmflow",
|
||
project_id=request.project_id,
|
||
source_chat_id=request.source_chat_id,
|
||
session_id=request.session_id,
|
||
force_new_session=bool(request.new_session),
|
||
thinking_mode=request.thinking_mode,
|
||
action_mode=request.action_mode,
|
||
sync_requirement=bool(request.sync_requirement),
|
||
)
|
||
return success_response(result)
|
||
|
||
|
||
@router.post("/command/dispatch")
|
||
@audited_write(caller="user")
|
||
async def dispatch_openclaw_command(
|
||
request: OpenClawCommandDispatchRequest,
|
||
current_user: User = Depends(require_manager_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
||
service = OpenClawCommanderService(db)
|
||
try:
|
||
result = await service.dispatch_command(
|
||
tenant_id=effective_tenant_id,
|
||
operator_user=current_user,
|
||
project_id=request.project_id,
|
||
instruction=request.instruction,
|
||
dry_run=request.dry_run,
|
||
max_operations=request.max_operations,
|
||
response_mode=request.response_mode,
|
||
)
|
||
except ValueError as exc:
|
||
detail = str(exc)
|
||
if detail == "project not found":
|
||
raise HTTPException(status_code=404, detail=detail) from exc
|
||
raise HTTPException(status_code=400, detail=detail) from exc
|
||
return success_response(result)
|
||
|
||
|
||
@router.post("/signal-sources/onboard")
|
||
@audited_write(caller="user")
|
||
async def onboard_signal_source(
|
||
request: SignalSourceOnboardRequest,
|
||
current_user: User = Depends(require_manager_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
||
signal = Signal(
|
||
tenant_id=effective_tenant_id,
|
||
source="PMFlow",
|
||
content=f"[PMFlow用户#{int(current_user.id)}]: {request.instruction[:1800]}",
|
||
impact_score=0.7,
|
||
priority="medium",
|
||
processed=False,
|
||
strategic_analysis=json.dumps(
|
||
{
|
||
"source": "pmflow_manual_signal_source_onboard",
|
||
"instruction": request.instruction[:2000],
|
||
"created_at": now_cn_aware().isoformat(),
|
||
},
|
||
ensure_ascii=False,
|
||
),
|
||
tasks_generated=json.dumps([], ensure_ascii=False),
|
||
matched_project_id=int(request.project_id),
|
||
title=(request.instruction[:80] or "新增信号源"),
|
||
signal_type="新需求",
|
||
summary=request.instruction[:300],
|
||
reasoning=json.dumps(["来自 AI Steward 手动信号源接入请求。"], ensure_ascii=False),
|
||
confidence=0.82,
|
||
recommended_action="enter_only",
|
||
status="待处理",
|
||
)
|
||
if not request.dry_run:
|
||
db.add(signal)
|
||
await db.flush()
|
||
await _ensure_project_signal_link(
|
||
db,
|
||
tenant_id=effective_tenant_id,
|
||
project_id=int(request.project_id),
|
||
signal_id=int(signal.id),
|
||
)
|
||
await db.commit()
|
||
await db.refresh(signal)
|
||
signal_id_value = int(signal.id) if not request.dry_run and getattr(signal, "id", None) is not None else None
|
||
|
||
service = SignalSourceOrchestrationService(db)
|
||
result = await service.orchestrate_from_signal(
|
||
tenant_id=effective_tenant_id,
|
||
project_id=int(request.project_id),
|
||
signal_id=signal_id_value,
|
||
initiating_text=request.instruction,
|
||
requested_via="pmflow",
|
||
operator_user_id=int(current_user.id),
|
||
dry_run=bool(request.dry_run),
|
||
response_mode=request.response_mode,
|
||
)
|
||
return success_response(
|
||
{
|
||
"signal_id": signal_id_value,
|
||
**result,
|
||
}
|
||
)
|
||
|
||
|
||
@router.get("/orchestration-runs/{trace_id}")
|
||
async def get_orchestration_run(
|
||
trace_id: str,
|
||
current_user: User = Depends(require_manager_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
||
row = await AIOrchestrationRunService(db).get_run(
|
||
tenant_id=effective_tenant_id,
|
||
trace_id=str(trace_id or "").strip(),
|
||
)
|
||
if row is None:
|
||
raise HTTPException(status_code=404, detail="orchestration run not found")
|
||
return success_response(AIOrchestrationRunService.serialize_run(row))
|
||
|
||
|
||
@router.get("/projects/{project_id}/signal-sources")
|
||
async def list_project_signal_sources(
|
||
project_id: int,
|
||
current_user: User = Depends(require_manager_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
||
items = await SignalSourceOrchestrationService(db).list_project_sources(
|
||
tenant_id=effective_tenant_id,
|
||
project_id=int(project_id),
|
||
)
|
||
return success_response(SignalSourceListResponse(items=items).model_dump())
|
||
|
||
|
||
@router.post("/signal-sources/repair-dirty")
|
||
@audited_write(caller="system")
|
||
async def repair_dirty_signal_sources(
|
||
request: SignalSourceRepairRequest,
|
||
current_user: User = Depends(require_manager_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
||
try:
|
||
rows = await SignalSourceRepairService(db).repair_registries(
|
||
tenant_id=effective_tenant_id,
|
||
project_id=int(request.project_id),
|
||
registry_ids=[int(item) for item in request.registry_ids],
|
||
sender=str(request.sender or "PMFlow修复器").strip()[:64] or "PMFlow修复器",
|
||
)
|
||
except SignalSourceRepairTimeoutError as exc:
|
||
raise HTTPException(status_code=504, detail=str(exc)) from exc
|
||
except SignalSourceRepairMissingSignalError as exc:
|
||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||
return success_response(
|
||
{
|
||
"project_id": int(request.project_id),
|
||
"items": rows,
|
||
}
|
||
)
|
||
|
||
@router.post("/project-from-text", response_model=ProjectFromTextResponse)
|
||
async def create_project_from_text(
|
||
request: ProjectFromTextRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
零配置项目创建 - 从一句话生成完整项目
|
||
|
||
示例输入:
|
||
```
|
||
我们要做一个 AI 客服工具,基于 GPT-4o mini,对标 XX 竞品,3 周上线
|
||
```
|
||
|
||
AI 将自动生成:
|
||
- 完整的项目配置
|
||
- 竞品分析信号
|
||
- 项目宪章
|
||
- 任务链(含依赖关系)
|
||
- 风险预警规则
|
||
"""
|
||
try:
|
||
service = get_ai_steward_service(db)
|
||
|
||
result = await service.create_project_from_natural_language(
|
||
input_text=request.input_text,
|
||
creator_id=int(current_user.id),
|
||
tenant_id=tenant_id
|
||
)
|
||
|
||
return ProjectFromTextResponse(**result)
|
||
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"项目创建失败: {str(e)}"
|
||
)
|
||
|
||
|
||
# ==================== Signal Capture Endpoints ====================
|
||
|
||
@router.post("/signal/capture-feishu")
|
||
@audited_write(caller="ai")
|
||
async def capture_feishu_signal(
|
||
request: FeishuCaptureRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
从飞书群聊捕获信号
|
||
|
||
AI 会自动判断消息是否包含有价值的项目或技术信息,
|
||
如果是,则创建信号并自动关联到相关项目
|
||
"""
|
||
try:
|
||
service = ProactiveSignalCaptureService(db)
|
||
result = await service.capture_from_feishu(
|
||
group_chat_id=request.group_chat_id,
|
||
message_content=request.message_content,
|
||
sender=request.sender,
|
||
tenant_id=tenant_id,
|
||
feishu_message_id=request.feishu_message_id,
|
||
)
|
||
response_payload = dict(result) if isinstance(result, dict) else {"result": result}
|
||
|
||
should_enqueue_reply = bool(response_payload.get("direct_reply_required"))
|
||
reply_text = str(response_payload.get("direct_reply_text") or "").strip() or "已收到,正在处理。"
|
||
source_message_id = (str(request.feishu_message_id or "").strip()[:128] or None)
|
||
normalized_chat_id = str(request.group_chat_id or "").strip()
|
||
if should_enqueue_reply and normalized_chat_id:
|
||
try:
|
||
outbox_id = await FeishuOutboxService.enqueue(
|
||
db,
|
||
tenant_id=int(tenant_id),
|
||
chat_id=normalized_chat_id,
|
||
text=reply_text,
|
||
source_message_id=source_message_id,
|
||
dedupe_seed=f"capture-feishu:{normalized_chat_id}:{str(request.sender or '').strip()[:64]}",
|
||
trace_id=str(response_payload.get("ai_trace_id") or "").strip()[:64] or None,
|
||
)
|
||
await db.commit()
|
||
response_payload["reply_outbox_enqueued"] = bool(outbox_id)
|
||
response_payload["reply_outbox_id"] = int(outbox_id) if outbox_id else None
|
||
except Exception as outbox_exc:
|
||
await db.rollback()
|
||
response_payload["reply_outbox_enqueued"] = False
|
||
response_payload["reply_outbox_error"] = str(outbox_exc)[:200]
|
||
return response_payload
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"飞书信号捕获失败: {str(e)}")
|
||
|
||
|
||
@router.post("/signal/capture-browser")
|
||
async def capture_browser_signal(
|
||
request: BrowserCaptureRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
从浏览器标签页捕获信号
|
||
|
||
监控用户访问的技术文档、竞品页面、GitHub issues等
|
||
"""
|
||
try:
|
||
service = ProactiveSignalCaptureService(db)
|
||
result = await service.capture_from_browser(
|
||
user_id=request.user_id,
|
||
url=request.url,
|
||
title=request.title,
|
||
content_preview=request.content_preview,
|
||
tenant_id=tenant_id
|
||
)
|
||
return result
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"浏览器信号捕获失败: {str(e)}")
|
||
|
||
|
||
@router.post("/signal/capture-github")
|
||
async def capture_github_signal(
|
||
request: GitHubCaptureRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
从GitHub事件捕获信号
|
||
|
||
监控竞品仓库更新、上游依赖更新等
|
||
"""
|
||
try:
|
||
service = ProactiveSignalCaptureService(db)
|
||
result = await service.capture_from_github(
|
||
repo_url=request.repo_url,
|
||
event_type=request.event_type,
|
||
event_data=request.event_data,
|
||
tenant_id=tenant_id
|
||
)
|
||
return result
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"GitHub信号捕获失败: {str(e)}")
|
||
|
||
|
||
@router.post("/signal/start-hn-monitor")
|
||
async def start_hn_monitor(
|
||
background_tasks: BackgroundTasks,
|
||
interval_minutes: int = 30,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
启动 HackerNews 定时监控(后台任务)
|
||
|
||
定期抓取 HackerNews 首页,分析技术趋势
|
||
"""
|
||
async def run_monitor():
|
||
service = ProactiveSignalCaptureService(db)
|
||
await service.start_hackernews_monitor(interval_minutes=interval_minutes, tenant_id=tenant_id)
|
||
|
||
background_tasks.add_task(run_monitor)
|
||
return {
|
||
"status": "started",
|
||
"message": f"HackerNews 监控已启动,间隔: {interval_minutes} 分钟"
|
||
}
|
||
|
||
|
||
# ==================== Cognitive Conflict Endpoints ====================
|
||
|
||
@router.get("/cognitive-conflicts", response_model=List[CognitiveConflictResponse])
|
||
async def get_cognitive_conflicts(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
project_id: Optional[int] = None
|
||
):
|
||
"""
|
||
获取认知冲突列表
|
||
|
||
AI 会自动识别团队中的隐性认知差异,例如:
|
||
- 产品说"用户留存提升10%",开发实现的是"DAU提升10%"
|
||
- 双方没有争论,但目标不一致,AI 会主动识别并推送对齐提醒
|
||
"""
|
||
from ..models import CognitiveConflict, TruthNode
|
||
from sqlalchemy.future import select
|
||
|
||
query = select(CognitiveConflict).where(
|
||
CognitiveConflict.tenant_id == tenant_id,
|
||
CognitiveConflict.status == 'active'
|
||
)
|
||
|
||
if project_id:
|
||
# 通过 truth_node 关联到项目
|
||
query = query.join(
|
||
TruthNode, CognitiveConflict.truth_node_id == TruthNode.id
|
||
).where(
|
||
TruthNode.project_id == project_id
|
||
)
|
||
|
||
result = await db.execute(query)
|
||
conflicts = result.scalars().all()
|
||
|
||
return [
|
||
CognitiveConflictResponse(
|
||
id=str(c.id),
|
||
type=c.conflict_type,
|
||
description=c.description,
|
||
severity="high" if "critical" in (c.description or "").lower() else "medium",
|
||
related_users=_parse_related_users(c.involved_user_ids),
|
||
)
|
||
for c in conflicts
|
||
]
|
||
|
||
|
||
@router.post("/conflicts/detect")
|
||
async def detect_conflicts(
|
||
request: ConflictDetectRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
检测项目中的认知冲突
|
||
|
||
支持检测类型:
|
||
- all: 全部检测(宪章冲突 + 定义冲突)
|
||
- charter: 仅检测宪章中的潜在冲突
|
||
- definition: 仅检测用户定义冲突
|
||
"""
|
||
try:
|
||
service = get_cognitive_conflict_service(db)
|
||
|
||
detected_conflicts = []
|
||
|
||
if request.detect_type in ["all", "charter"]:
|
||
charter_conflicts = await service.detect_charter_conflicts(
|
||
project_id=request.project_id,
|
||
tenant_id=tenant_id
|
||
)
|
||
detected_conflicts.extend(charter_conflicts)
|
||
|
||
if request.detect_type in ["all", "definition"]:
|
||
definition_conflicts = await service.detect_user_definition_conflicts(
|
||
project_id=request.project_id,
|
||
tenant_id=tenant_id
|
||
)
|
||
detected_conflicts.extend(definition_conflicts)
|
||
|
||
return {
|
||
"project_id": request.project_id,
|
||
"detected_count": len(detected_conflicts),
|
||
"conflicts": detected_conflicts
|
||
}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"冲突检测失败: {str(e)}")
|
||
|
||
|
||
@router.post("/conflicts/{conflict_id}/propose-resolution")
|
||
async def propose_conflict_resolution(
|
||
conflict_id: int,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
AI 生成认知冲突解决方案
|
||
|
||
为冲突双方生成对齐方案:
|
||
1. 分析冲突根因
|
||
2. 提出妥协方案
|
||
3. 生成可执行的对齐计划
|
||
"""
|
||
try:
|
||
service = get_cognitive_conflict_service(db)
|
||
resolution = await service.propose_alignment_resolution(
|
||
conflict_id=conflict_id,
|
||
tenant_id=tenant_id
|
||
)
|
||
return resolution
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"方案生成失败: {str(e)}")
|
||
|
||
|
||
@router.post("/conflicts/{conflict_id}/align")
|
||
async def trigger_async_alignment(
|
||
conflict_id: int,
|
||
user1_id: int,
|
||
user2_id: int,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
触发异步 1v1 认知对齐
|
||
|
||
为冲突双方创建独立的对齐任务:
|
||
1. 发送对齐邀请
|
||
2. 提供背景信息
|
||
3. 收集双方观点
|
||
4. AI 生成共识方案
|
||
"""
|
||
try:
|
||
service = get_cognitive_conflict_service(db)
|
||
alignment = await service.trigger_async_alignment(
|
||
conflict_id=conflict_id,
|
||
user1_id=user1_id,
|
||
user2_id=user2_id,
|
||
tenant_id=tenant_id
|
||
)
|
||
return alignment
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"对齐触发失败: {str(e)}")
|
||
|
||
|
||
@router.post("/cognitive-conflicts/{conflict_id}/resolve")
|
||
async def resolve_cognitive_conflict(
|
||
conflict_id: int,
|
||
resolution_note: str,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
解决认知冲突
|
||
|
||
AI 会生成解决方案建议,双方确认后标记为已解决
|
||
"""
|
||
from ..models import CognitiveConflict
|
||
from sqlalchemy.future import select
|
||
|
||
result = await db.execute(
|
||
select(CognitiveConflict).where(
|
||
CognitiveConflict.id == conflict_id,
|
||
CognitiveConflict.tenant_id == tenant_id
|
||
)
|
||
)
|
||
conflict = result.scalar_one_or_none()
|
||
|
||
if not conflict:
|
||
raise HTTPException(status_code=404, detail="冲突记录不存在")
|
||
|
||
conflict.status = 'resolved'
|
||
conflict.resolved_at = _utc_now_naive()
|
||
conflict.description += f"\n\n解决方案({current_user.username}): {resolution_note}"
|
||
|
||
await db.commit()
|
||
|
||
return {"message": "冲突已标记为已解决"}
|
||
|
||
|
||
@router.get("/conflicts/task-mismatch/{task_id}")
|
||
async def check_task_charter_mismatch(
|
||
task_id: int,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
检测任务与宪章的一致性
|
||
|
||
检查:
|
||
1. 任务描述是否与宪章目标一致
|
||
2. 技术选型是否符合约束
|
||
3. 是否偏离项目方向
|
||
"""
|
||
from ..models import Task, TruthNode
|
||
from sqlalchemy.future import select
|
||
|
||
try:
|
||
# 获取任务
|
||
result = await db.execute(
|
||
select(Task).where(
|
||
Task.id == task_id,
|
||
Task.tenant_id == tenant_id
|
||
)
|
||
)
|
||
task = result.scalar_one_or_none()
|
||
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
|
||
# 获取宪章
|
||
charter_result = await db.execute(
|
||
select(TruthNode).where(
|
||
TruthNode.id == task.truth_node_id,
|
||
TruthNode.tenant_id == tenant_id
|
||
)
|
||
)
|
||
charter = charter_result.scalar_one_or_none()
|
||
|
||
if not charter:
|
||
raise HTTPException(status_code=404, detail="宪章不存在")
|
||
|
||
# 检测一致性
|
||
service = get_cognitive_conflict_service(db)
|
||
mismatch = await service.detect_task_charter_mismatch(
|
||
task=task,
|
||
charter_content=charter.content,
|
||
tenant_id=tenant_id
|
||
)
|
||
|
||
if mismatch:
|
||
return {
|
||
"is_aligned": False,
|
||
"mismatch": mismatch
|
||
}
|
||
|
||
return {
|
||
"is_aligned": True,
|
||
"message": "任务与宪章一致"
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"一致性检测失败: {str(e)}")
|
||
|
||
|
||
# ==================== Other AI Steward Endpoints ====================
|
||
|
||
@router.get("/daily-briefing")
|
||
async def get_daily_briefing(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
获取个性化每日简报
|
||
|
||
AI 为每个角色生成定制化简报,只包含:
|
||
- 与你相关的任务变更
|
||
- 风险提醒
|
||
- 需要对齐的事项
|
||
- 高影响信号
|
||
- 团队更新
|
||
|
||
彻底过滤无关信息,替代传统日报/周报
|
||
"""
|
||
try:
|
||
service = get_daily_briefing_service(db)
|
||
briefing = await service.generate_personalized_briefing(
|
||
user_id=int(current_user.id),
|
||
tenant_id=tenant_id
|
||
)
|
||
return briefing
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"生成简报失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/daily-briefing/team/{project_id}")
|
||
async def get_team_briefing(
|
||
project_id: int,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
获取团队简报(给管理者)
|
||
|
||
包含:
|
||
- 整体进度
|
||
- 每个成员的工作量
|
||
- 风险和阻塞
|
||
- 需要决策的事项
|
||
"""
|
||
try:
|
||
service = get_daily_briefing_service(db)
|
||
briefing = await service.generate_team_briefing(
|
||
project_id=project_id,
|
||
tenant_id=tenant_id
|
||
)
|
||
return briefing
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"生成团队简报失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/analyze-blocker")
|
||
async def analyze_blocker(
|
||
code_context: str,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
分析代码阻塞
|
||
|
||
当开发在代码注释中提到「这个接口达不到效果」时,
|
||
AI 自动触发分析,推荐替代方案或生成代码片段
|
||
"""
|
||
from ..services.llm import LLMService
|
||
|
||
service = get_ai_steward_service(db)
|
||
|
||
prompt = f"""分析以下代码/注释中提到的问题,提供解决方案:
|
||
|
||
代码上下文:
|
||
{code_context}
|
||
|
||
请提供:
|
||
1. 问题分析
|
||
2. 推荐的替代方案
|
||
3. 示例代码(如适用)
|
||
|
||
以 JSON 格式返回。"""
|
||
|
||
try:
|
||
llm = LLMService()
|
||
response = await llm.generate(
|
||
prompt,
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
)
|
||
|
||
return {
|
||
'analysis': response,
|
||
'suggestions': [],
|
||
'auto_generated': True
|
||
}
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"分析失败: {str(e)}"
|
||
)
|
||
|
||
|
||
# ==================== Blocker Detection Endpoints ====================
|
||
|
||
class CodeCommentScanRequest(BaseModel):
|
||
"""代码注释扫描请求"""
|
||
code_content: str
|
||
file_path: str
|
||
line_number: int
|
||
project_id: int
|
||
|
||
|
||
class BlockerResolveRequest(BaseModel):
|
||
"""阻塞解决请求"""
|
||
blocker_id: int
|
||
resolution_note: str
|
||
|
||
|
||
@router.post("/blockers/scan-comment")
|
||
async def scan_code_comment_for_blockers(
|
||
request: CodeCommentScanRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
扫描代码注释,检测阻塞
|
||
|
||
自动识别阻塞模式:
|
||
- "等待 X 接口完成"
|
||
- "这个 API 达不到效果"
|
||
- "由于 Y 的限制,暂时跳过"
|
||
|
||
如果检测到阻塞,AI 会自动:
|
||
1. 分析严重程度
|
||
2. 生成解决方案建议
|
||
3. 创建追踪任务(如需要)
|
||
"""
|
||
try:
|
||
service = get_blocker_detection_service(db)
|
||
result = await service.scan_code_comment(
|
||
code_content=request.code_content,
|
||
file_path=request.file_path,
|
||
line_number=request.line_number,
|
||
project_id=request.project_id,
|
||
user_id=int(current_user.id),
|
||
tenant_id=tenant_id
|
||
)
|
||
|
||
if result:
|
||
return {
|
||
'status': 'blocker_detected',
|
||
'blocker': result
|
||
}
|
||
|
||
return {
|
||
'status': 'no_blocker',
|
||
'message': '未检测到阻塞'
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"扫描失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/blockers/detect-tasks")
|
||
async def detect_task_blockers(
|
||
background_tasks: BackgroundTasks,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
检测所有任务中的阻塞
|
||
|
||
扫描所有超过3天未更新的进行中任务,
|
||
使用 AI 判断是否存在阻塞
|
||
"""
|
||
try:
|
||
service = get_blocker_detection_service(db)
|
||
blockers = await service.detect_task_blockers(tenant_id=tenant_id)
|
||
|
||
return {
|
||
'total_detected': len(blockers),
|
||
'blockers': blockers
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"检测失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/blockers/active")
|
||
async def get_active_blockers(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
获取所有活跃的阻塞
|
||
|
||
返回所有未解决的阻塞,按严重程度排序
|
||
"""
|
||
try:
|
||
service = get_blocker_detection_service(db)
|
||
blockers = await service.get_active_blockers(tenant_id=tenant_id)
|
||
|
||
# 按严重程度排序
|
||
severity_order = {'high': 0, 'medium': 1, 'low': 2}
|
||
blockers.sort(key=lambda b: severity_order.get(b['severity'], 1))
|
||
|
||
return {
|
||
'total': len(blockers),
|
||
'blockers': blockers
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"获取失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/blockers/resolve")
|
||
async def resolve_blocker(
|
||
request: BlockerResolveRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
标记阻塞为已解决
|
||
|
||
记录解决方案并关闭阻塞记录
|
||
"""
|
||
try:
|
||
service = get_blocker_detection_service(db)
|
||
result = await service.resolve_blocker(
|
||
blocker_id=request.blocker_id,
|
||
resolution_note=request.resolution_note,
|
||
tenant_id=tenant_id
|
||
)
|
||
|
||
return result
|
||
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"解决失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/stats")
|
||
async def get_ai_steward_stats(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
获取 AI Steward 统计数据
|
||
|
||
展示 AI 为团队节省的工作量:
|
||
- 自动捕获的信号数
|
||
- 避免的认知冲突数
|
||
- 自动解决的阻塞数
|
||
- 节省的会议时间(小时)
|
||
"""
|
||
from ..models import Signal, CognitiveConflict
|
||
from sqlalchemy import func, text
|
||
|
||
# 统计信号数量
|
||
week_ago = _utc_now_naive() - timedelta(days=7)
|
||
month_ago = _utc_now_naive() - timedelta(days=30)
|
||
|
||
signal_result = await db.execute(
|
||
select(func.count(Signal.id)).where(
|
||
Signal.tenant_id == tenant_id,
|
||
Signal.created_at >= week_ago
|
||
)
|
||
)
|
||
signals_captured = signal_result.scalar() or 0
|
||
|
||
# 统计高影响信号
|
||
high_impact_result = await db.execute(
|
||
select(func.count(Signal.id)).where(
|
||
Signal.tenant_id == tenant_id,
|
||
Signal.impact_score >= 0.8,
|
||
Signal.created_at >= week_ago
|
||
)
|
||
)
|
||
high_impact_count = high_impact_result.scalar() or 0
|
||
|
||
# 统计认知冲突
|
||
conflict_result = await db.execute(
|
||
select(func.count(CognitiveConflict.id)).where(
|
||
CognitiveConflict.tenant_id == tenant_id,
|
||
CognitiveConflict.status.in_(['detected', 'active'])
|
||
)
|
||
)
|
||
active_conflicts = conflict_result.scalar() or 0
|
||
|
||
# 统计已解决的冲突
|
||
resolved_conflict_result = await db.execute(
|
||
select(func.count(CognitiveConflict.id)).where(
|
||
CognitiveConflict.tenant_id == tenant_id,
|
||
CognitiveConflict.status == 'resolved',
|
||
CognitiveConflict.resolved_at >= month_ago
|
||
)
|
||
)
|
||
conflicts_resolved = resolved_conflict_result.scalar() or 0
|
||
|
||
# 统计阻塞
|
||
blocker_result = await db.execute(
|
||
text("""
|
||
SELECT COUNT(*) FROM cognitive_conflicts
|
||
WHERE tenant_id = :tenant_id
|
||
AND conflict_type IN ('code_blocker', 'task_blocker')
|
||
AND status = 'detected'
|
||
"""),
|
||
{"tenant_id": tenant_id}
|
||
)
|
||
active_blockers = blocker_result.scalar() or 0
|
||
|
||
# 活跃项目数
|
||
from ..models import Project
|
||
project_result = await db.execute(
|
||
select(func.count(Project.id)).where(
|
||
Project.tenant_id == tenant_id,
|
||
Project.status == 'active'
|
||
)
|
||
)
|
||
active_projects = project_result.scalar() or 0
|
||
|
||
# 估算节省时间(每个冲突/阻塞避免约2小时会议)
|
||
hours_saved = conflicts_resolved * 2
|
||
|
||
stats = {
|
||
'signals_captured': signals_captured,
|
||
'high_impact_signals': high_impact_count,
|
||
'active_conflicts': active_conflicts,
|
||
'conflicts_resolved': conflicts_resolved,
|
||
'active_blockers': active_blockers,
|
||
'active_projects': active_projects,
|
||
'hours_saved': hours_saved,
|
||
'last_updated': now_cn_aware().isoformat()
|
||
}
|
||
|
||
return stats
|
||
|
||
|
||
@router.post("/sync-signal")
|
||
async def sync_external_signal(
|
||
source: str,
|
||
content: str,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
手动同步外部信号
|
||
|
||
支持从以下来源同步:
|
||
- 飞书群聊
|
||
- GitHub commits/PRs
|
||
- 浏览器标签页
|
||
"""
|
||
from ..models import Signal
|
||
from ..services.ai_steward import get_ai_steward_service
|
||
|
||
service = get_ai_steward_service(db)
|
||
|
||
# AI 分析信号重要性
|
||
prompt = f"""分析以下信号的重要性:
|
||
|
||
来源:{source}
|
||
内容:{content}
|
||
|
||
请评估:
|
||
1. impact_score (0-1)
|
||
2. priority (low/medium/high/critical)
|
||
3. 是否需要立即行动
|
||
|
||
以 JSON 格式返回。"""
|
||
|
||
try:
|
||
from ..services.llm import LLMService
|
||
llm = LLMService()
|
||
response = await llm.generate(
|
||
prompt,
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
)
|
||
|
||
# 创建信号
|
||
signal = Signal(
|
||
source=source,
|
||
content=content,
|
||
impact_score=0.7,
|
||
priority='medium',
|
||
processed=False,
|
||
tenant_id=tenant_id
|
||
)
|
||
db.add(signal)
|
||
await db.commit()
|
||
|
||
return {
|
||
'signal_id': str(signal.id),
|
||
'analysis': response,
|
||
'status': 'captured'
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"信号同步失败: {str(e)}"
|
||
)
|
||
|
||
|
||
# ==================== Event-Driven Workflow Endpoints ====================
|
||
|
||
class WorkflowTriggerRequest(BaseModel):
|
||
"""工作流触发请求"""
|
||
event_type: str
|
||
event_data: Dict[str, Any]
|
||
|
||
|
||
@router.post("/workflow/trigger")
|
||
async def trigger_workflow(
|
||
request: WorkflowTriggerRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
触发事件驱动工作流
|
||
|
||
支持的事件类型:
|
||
- high_impact_signal: 高影响信号到达
|
||
- blocker_detected: 阻塞检测
|
||
- cognitive_conflict: 认知冲突
|
||
- tech_stack_change: 技术栈变更
|
||
- competitor_update: 竞品动态
|
||
- task_completed: 任务完成
|
||
- task_blocked: 任务阻塞
|
||
- project_risk: 项目风险
|
||
|
||
工作流会自动:
|
||
1. 调整项目优先级
|
||
2. 创建响应任务
|
||
3. 通知相关角色
|
||
4. 更新依赖关系
|
||
"""
|
||
try:
|
||
engine = get_event_workflow_engine(db)
|
||
result = await engine.trigger_workflow(
|
||
event_type=request.event_type,
|
||
event_data=request.event_data,
|
||
tenant_id=tenant_id
|
||
)
|
||
return result
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"工作流触发失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/workflow/signal/{signal_id}")
|
||
async def trigger_signal_workflow(
|
||
signal_id: int,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
当接收到高影响信号时自动触发工作流
|
||
|
||
检查信号的 impact_score,如果超过阈值(0.85),
|
||
自动触发工作流来调整相关项目的优先级
|
||
"""
|
||
from ..models import Signal
|
||
from sqlalchemy.future import select
|
||
|
||
signal_result = await db.execute(
|
||
select(Signal).where(
|
||
Signal.id == signal_id,
|
||
Signal.tenant_id == tenant_id
|
||
)
|
||
)
|
||
signal = signal_result.scalar_one_or_none()
|
||
|
||
if not signal:
|
||
raise HTTPException(status_code=404, detail="信号不存在")
|
||
|
||
# 检查是否达到阈值
|
||
if signal.impact_score < 0.85:
|
||
return {
|
||
'status': 'ignored',
|
||
'reason': f'信号影响分数 {signal.impact_score} 低于阈值 0.85'
|
||
}
|
||
|
||
# 触发高影响信号工作流
|
||
engine = get_event_workflow_engine(db)
|
||
result = await engine.trigger_workflow(
|
||
event_type=WorkflowEventType.HIGH_IMPACT_SIGNAL,
|
||
event_data={'signal_id': signal_id},
|
||
tenant_id=tenant_id
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.get("/workflow/status")
|
||
async def get_workflow_status(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
获取工作流状态
|
||
|
||
显示最近触发的工作流及其结果
|
||
"""
|
||
from ..models import AICallLog
|
||
from sqlalchemy.future import select
|
||
|
||
# 获取最近的工作流记录
|
||
# 这里简化处理,实际应该有专门的工作流记录表
|
||
return {
|
||
'active_workflows': [],
|
||
'recent_triggers': [],
|
||
'message': '工作流状态追踪'
|
||
}
|
||
|
||
|
||
@router.post("/workflow/auto-enable")
|
||
async def enable_auto_workflow(
|
||
project_id: int,
|
||
enable: bool = True,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
启用/禁用项目的自动工作流
|
||
|
||
启用后,当以下情况发生时会自动触发工作流:
|
||
- 高影响信号到达(>0.85)
|
||
- 任务阻塞检测
|
||
- 认知冲突检测
|
||
"""
|
||
from ..models import Project
|
||
from sqlalchemy.future import select
|
||
|
||
project_result = await db.execute(
|
||
select(Project).where(
|
||
Project.id == project_id,
|
||
Project.tenant_id == tenant_id
|
||
)
|
||
)
|
||
project = project_result.scalar_one_or_none()
|
||
|
||
if not project:
|
||
raise HTTPException(status_code=404, detail="项目不存在")
|
||
|
||
# 这里应该设置项目的 auto_workflow 标志
|
||
# 由于 Project 模型可能没有这个字段,暂时返回成功
|
||
return {
|
||
'project_id': project_id,
|
||
'auto_workflow': enable,
|
||
'message': f'自动工作流已{"启用" if enable else "禁用"}'
|
||
}
|
||
|
||
|
||
# ==================== AI Config Suggestion ====================
|
||
|
||
@router.get("/config/suggest")
|
||
async def get_ai_config_suggestion(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id)
|
||
):
|
||
"""
|
||
AI 建议配置方案
|
||
|
||
根据团队现状,智能推荐应该优先配置哪些集成源
|
||
"""
|
||
from ..models import Integration, Project, Signal
|
||
from sqlalchemy.future import select
|
||
from sqlalchemy import func
|
||
|
||
# 检查已配置的集成
|
||
configured_sources = []
|
||
|
||
# 检查飞书配置
|
||
feishu_result = await db.execute(
|
||
select(Integration).where(
|
||
Integration.tenant_id == tenant_id,
|
||
Integration.type == 'feishu',
|
||
Integration.is_active == True
|
||
)
|
||
)
|
||
if feishu_result.scalar_one_or_none():
|
||
configured_sources.append('feishu')
|
||
|
||
# 检查活跃项目数
|
||
project_result = await db.execute(
|
||
select(func.count(Project.id)).where(
|
||
Project.tenant_id == tenant_id,
|
||
Project.status == 'active'
|
||
)
|
||
)
|
||
active_projects = project_result.scalar() or 0
|
||
|
||
# 检查近期信号数量
|
||
signal_result = await db.execute(
|
||
select(func.count(Signal.id)).where(
|
||
Signal.tenant_id == tenant_id,
|
||
Signal.created_at >= (_utc_now_naive() - timedelta(days=7))
|
||
)
|
||
)
|
||
recent_signals = signal_result.scalar() or 0
|
||
|
||
# AI 分析并推荐配置
|
||
recommended = []
|
||
reasoning_parts = []
|
||
|
||
# 根据项目数量推荐
|
||
if active_projects > 0:
|
||
if 'feishu' not in configured_sources:
|
||
recommended.append('feishu')
|
||
reasoning_parts.append(f"您有 {active_projects} 个活跃项目,建议配置飞书群聊集成以自动捕获需求讨论")
|
||
|
||
# 根据信号量推荐
|
||
if recent_signals < 10:
|
||
if 'hn' not in [s.split('_')[0] for s in configured_sources]:
|
||
recommended.append('hn')
|
||
reasoning_parts.append("近期信号较少,建议启用 HackerNews 监控获取更多技术趋势")
|
||
|
||
# 默认推荐
|
||
if not recommended:
|
||
if 'feishu' not in configured_sources:
|
||
recommended.append('feishu')
|
||
if 'hn' not in [s.split('_')[0] for s in configured_sources]:
|
||
recommended.append('hn')
|
||
reasoning_parts.append("建议配置基础集成源以启动 AI 主动捕获能力")
|
||
|
||
reasoning = ";".join(reasoning_parts) if reasoning_parts else "所有基础集成已配置,AI 主动捕获能力已全面启用"
|
||
|
||
# 估算影响
|
||
impact_levels = {
|
||
0: "暂无明显影响",
|
||
1: "预计每周节省 2 小时手动录入时间",
|
||
2: "预计每周节省 4 小时手动录入时间,捕获 10+ 个需求信号",
|
||
3: "预计每周节省 6 小时会议时间,自动对齐 5+ 个认知冲突",
|
||
4: "预计每周节省 8 小时会议时间,主动捕获 20+ 个需求信号"
|
||
}
|
||
impact = impact_levels.get(len(recommended), impact_levels[4])
|
||
|
||
return {
|
||
'recommended_sources': recommended,
|
||
'reasoning': reasoning,
|
||
'estimated_impact': impact,
|
||
'current_config': {
|
||
'feishu_configured': 'feishu' in configured_sources,
|
||
'hn_configured': any('hn' in s for s in configured_sources),
|
||
'github_configured': any('github' in s for s in configured_sources),
|
||
'browser_installed': False # 需要前端检测
|
||
}
|
||
}
|