""" 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 # 需要前端检测 } }