from __future__ import annotations from .shared import * from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project, _ensure_release, _ensure_review, _parse_due_date from ...services.sstc.governance import _assert_charter_aligned, _require_manager from ...services.sstc.review_utils import _normalize_review_content from ...services.sstc.signal_core import _as_dict, _parse_node_id, _parse_project_id, _sync_pmflow_rag_async router = APIRouter(tags=["sstc"]) @router.patch("/projects/{project_id}/reviews/{review_node_id}/content") async def patch_review_content( project_id: str, review_node_id: str, body: ReviewContentPatchRequest, 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) rid = _parse_node_id(review_node_id, "review_node_id") tenant_id = int(current_user.tenant_id) await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid) review_node = await _ensure_review( db, tenant_id=tenant_id, project_id=pid, review_node_id=rid, ) existing = _as_dict(review_node.content) merged = dict(existing) merged.update(body.content or {}) normalized = _normalize_review_content(merged) normalized["release_node_id"] = existing.get("release_node_id") normalized["charter_id"] = existing.get("charter_id") review_node.content = normalized await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="review", entity_id=str(rid), action="update_content", after_data={ "improvement_count": len(normalized.get("improvements") or []), "summary_len": len(str(normalized.get("summary") or "")), }, ) await db.commit() _sync_pmflow_rag_async( tenant_id=tenant_id, record_type="review", record_id=int(review_node.id), project_id=pid, title=review_node.title, content=_as_dict(review_node.content), ) return success_response( { "id": str(review_node.id), "status": review_node.status, "content": _as_dict(review_node.content), } ) @router.post("/projects/{project_id}/reviews/{review_node_id}/improvements/convert-task") async def convert_review_improvement_to_task( project_id: str, review_node_id: str, body: ReviewImprovementToTaskRequest, 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) rid = _parse_node_id(review_node_id, "review_node_id") tenant_id = int(current_user.tenant_id) await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid) review_node = await _ensure_review( db, tenant_id=tenant_id, project_id=pid, review_node_id=rid, ) review_content = _normalize_review_content(_as_dict(review_node.content)) improvements = review_content.get("improvements") or [] if body.improvement_index >= len(improvements): raise HTTPException(status_code=400, detail="改进项索引超出范围") item = improvements[body.improvement_index] if not isinstance(item, dict): raise HTTPException(status_code=400, detail="改进项数据无效") release_node_id = int(review_node.parent_id or review_content.get("release_node_id") or 0) if release_node_id <= 0: raise HTTPException(status_code=409, detail="复盘未关联发布计划,无法生成任务") release_node = await _ensure_release( db, tenant_id=tenant_id, project_id=pid, release_node_id=release_node_id, ) release_content = _as_dict(release_node.content) charter_id = ( int(body.charter_id) if body.charter_id else int(release_content.get("charter_id") or release_node.parent_id or 0) ) if charter_id <= 0: raise HTTPException(status_code=409, detail="无法定位宪章,改进项无法转任务") charter = await _ensure_charter( db, tenant_id=tenant_id, project_id=pid, charter_id=charter_id, ) _assert_charter_aligned(charter) title = compact_title( body.title or str(item.get("title") or "").strip() or "复盘改进项任务", max_len=32, ) if not title: raise HTTPException(status_code=400, detail="改进项标题为空") desc_parts = [ str(body.description or "").strip(), str(item.get("description") or "").strip(), f"来源复盘: {review_node.title}", ] description = "\n".join([part for part in desc_parts if part]) due_date = _parse_due_date(body.due_date) task = await persist_task( db, tenant_id=tenant_id, truth_node_id=charter_id, project_id=pid, draft=build_task_create_draft( title=title, description=description or None, assignee_id=item.get("owner_id"), required_skill_tag_ids=[], status="todo", priority=body.priority, estimated_hours=body.estimated_hours, due_date=due_date, ), actor_user_id=int(current_user.id), action="create_from_review_improvement", remark="sstc.review_improvement_to_task", commit=False, refresh_workload=False, ) item["task_id"] = int(task.id) item["status"] = "converted" review_content["improvements"] = improvements review_content["release_node_id"] = release_node_id review_content["charter_id"] = charter_id review_node.content = review_content await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="task", entity_id=str(task.id), action="create_from_review_improvement", after_data={ "review_id": rid, "improvement_index": body.improvement_index, "charter_id": charter_id, }, ) await db.commit() await db.refresh(task) _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 "", ) _sync_pmflow_rag_async( tenant_id=tenant_id, record_type="review", record_id=int(review_node.id), project_id=pid, title=review_node.title, content=_as_dict(review_node.content), ) return success_response( { "task": { "id": str(task.id), "title": task.title, "status": task.status, "priority": task.priority, "charter_id": str(charter_id), }, "review_id": str(review_node.id), "improvement_index": body.improvement_index, } )