from __future__ import annotations import re from .shared import * from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project from ...services.sstc.governance import _assert_charter_aligned, _require_manager from ...services.sstc.signal_core import _as_dict, _parse_node_id, _parse_project_id, _sync_pmflow_rag_async router = APIRouter(tags=["sstc"]) _TITLE_NORMALIZE_RE = re.compile(r"[\s\-_]+") def _task_title_key(value: str) -> str: text = str(value or "").strip().lower() return _TITLE_NORMALIZE_RE.sub("", text) @router.post("/projects/{project_id}/charters/{charter_id}/tasks/ai-breakdown") async def ai_breakdown_tasks( project_id: str, charter_id: str, permission_user: User = Depends(require_project_member_or_manager), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _ = permission_user _require_manager(current_user) pid = _parse_project_id(project_id) cid = _parse_node_id(charter_id, "charter_id") tenant_id = int(current_user.tenant_id) await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid) charter = await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=cid) _assert_charter_aligned(charter) prompt = f"""请基于以下需求宪章拆解 5-8 条任务,返回 JSON 数组: [ {{"task_name":"", "type":"backend/frontend/ai/devops", "estimated_hours": 4.0, "priority":"P0/P1/P2"}} ] 宪章标题:{charter.title} 宪章内容:{_as_dict(charter.content)} """ try: rows = await llm_service.generate_task_breakdown_async( prompt, db=db, tenant_id=tenant_id, project_id=pid, ) except RuntimeError as exc: raise HTTPException(status_code=403, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=500, detail=f"AI 拆解失败: {exc}") from exc existing_task_title_result = await db.execute( select(Task.title).where( and_( Task.truth_node_id == cid, Task.project_id == pid, Task.tenant_id == tenant_id, ) ) ) existing_title_keys = { _task_title_key(str(title)) for title in existing_task_title_result.scalars().all() if str(title or "").strip() } seen_title_keys = set(existing_title_keys) created = [] skipped_duplicates = 0 prompt_tokens = max(len(prompt) // 4, 1) completion_tokens = 0 for item in rows or []: title = compact_title((item.get("task_name") or item.get("title") or ""), max_len=32) if not title: continue title_key = _task_title_key(title) if not title_key: continue if title_key in seen_title_keys: skipped_duplicates += 1 continue seen_title_keys.add(title_key) priority = str(item.get("priority") or "P1").lower() mapped_priority = "high" if priority == "p0" else "medium" if priority == "p1" else "low" task = await persist_task( db, tenant_id=tenant_id, truth_node_id=cid, project_id=pid, draft=build_task_create_draft( title=title, description=item.get("description"), required_skill_tag_ids=[], priority=mapped_priority, status="todo", estimated_hours=float(item.get("estimated_hours") or 0) or None, ), actor_user_id=int(current_user.id), action="task.create", remark="sstc.ai_breakdown", commit=False, refresh_workload=False, ) created.append(task) completion_tokens += max(len(title) // 2, 1) db.add( AICallLog( tenant_id=tenant_id, user_id=int(current_user.id), function_name="sstc_ai_breakdown", prompt=prompt, response=f"created_count={len(created)},skipped_duplicates={skipped_duplicates}", tokens_used=prompt_tokens + completion_tokens, cost=((prompt_tokens * 0.00015 + completion_tokens * 0.0006) / 1000), status="success", ) ) await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="task", entity_id=str(cid), action="ai_breakdown", after_data={"created_count": len(created), "skipped_duplicates": skipped_duplicates}, ) await db.commit() for task in created: _sync_pmflow_rag_async( tenant_id=tenant_id, record_type="task", record_id=int(task.id), project_id=pid, title=task.title, content=task.description or "", ) await context_cache_service.invalidate_system_context(tenant_id=tenant_id) return success_response( { "created_count": len(created), "skipped_duplicates": skipped_duplicates, "items": [ {"id": str(t.id), "title": t.title, "priority": t.priority, "status": t.status} for t in created ], } )