PMFlow/backend/app/api/ai_steward.py

1431 lines
42 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.

"""
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.openclaw_commander_service import OpenClawCommanderService
from ..services.signal_source_orchestration_service import SignalSourceOrchestrationService
from ..schemas.rag import RAGAskRequest
router = APIRouter(prefix="/ai-steward", tags=["AI Steward"])
def _utc_now_naive() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
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]]
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")
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")
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": datetime.now(timezone.utc).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("/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")
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,
)
return result
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': datetime.now(timezone.utc).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 # 需要前端检测
}
}