from __future__ import annotations from .shared import * from ...services.sstc.charter_prefill_builder import _build_ai_first_charter_content from ...services.sstc.charter_prefill_utils import _has_complete_module_data, _merge_prefill_content from ...services.sstc.charter_seed_utils import _extract_charter_signal_ids from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project from ...services.sstc.signal_core import _as_dict, _parse_node_id, _parse_project_id, _sync_pmflow_rag_async router = APIRouter(tags=["sstc"]) class CharterAIPrefillRequest(BaseModel): force: bool = False raw_text: Optional[str] = None @router.post("/projects/{project_id}/charters/{charter_id}/ai-prefill") async def ai_prefill_charter( project_id: str, charter_id: str, body: CharterAIPrefillRequest = Body(default_factory=CharterAIPrefillRequest), permission_user: User = Depends(require_project_member_or_manager), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _ = permission_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) before_content = _as_dict(charter.content) before_status = str(charter.status or "") if not body.force and _has_complete_module_data(before_content): return success_response( { "id": str(charter.id), "title": charter.title, "content": before_content, "status": charter.status, "version": charter.version, "prefilled": False, "skipped": True, } ) signal_ids = _extract_charter_signal_ids(before_content, charter.signal_id) signal_rows: list[Signal] = [] if signal_ids: signal_rows = ( await db.execute( select(Signal).where( and_( Signal.tenant_id == tenant_id, Signal.id.in_(signal_ids), ) ) ) ).scalars().all() raw_text = str(body.raw_text or before_content.get("raw_source_text") or "").strip() if not signal_rows and not raw_text: raise HTTPException(status_code=400, detail="未找到可用于预填的信号源或原始文本") generated = await _build_ai_first_charter_content( db=db, tenant_id=tenant_id, project_id=pid, signal_rows=signal_rows, signal_ids=signal_ids, raw_text=raw_text, ) merged = _merge_prefill_content( before_content, generated, overwrite_existing=bool(body.force) ) merged["signal_ids"] = signal_ids if raw_text: merged["raw_source_text"] = raw_text charter.content = merged charter.status = "draft" await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="charter", entity_id=str(cid), action="ai_prefill", before_data={"content": before_content, "status": before_status}, after_data={"content": merged, "status": "draft"}, ) await db.commit() await db.refresh(charter) _sync_pmflow_rag_async( tenant_id=tenant_id, record_type="charter", record_id=int(charter.id), project_id=pid, title=charter.title, content=_as_dict(charter.content), ) return success_response( { "id": str(charter.id), "title": charter.title, "content": _as_dict(charter.content), "status": charter.status, "version": charter.version, "prefilled": True, "skipped": False, } ) @router.post("/projects/{project_id}/charters/{charter_id}/alignment/start") async def start_alignment( project_id: str, charter_id: str, body: AlignmentStartRequest, permission_user: User = Depends(require_project_member_or_manager), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _ = permission_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) before = {"status": charter.status} charter.status = "aligning" await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="charter", entity_id=str(cid), action="start_alignment", before_data=before, after_data={"status": charter.status, "note": body.note}, ) await db.commit() return success_response({"id": str(cid), "status": charter.status}) @router.post("/projects/{project_id}/charters/{charter_id}/alignment/confirm") async def confirm_alignment( project_id: str, charter_id: str, body: AlignmentConfirmRequest, permission_user: User = Depends(require_project_member_or_manager), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _ = permission_user pid = _parse_project_id(project_id) cid = _parse_node_id(charter_id, "charter_id") tenant_id = int(current_user.tenant_id) svc = SSTCService(db, tenant_id=tenant_id) await _ensure_project(svc, pid) charter = await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=cid) row_result = await db.execute( select(AlignmentRecord).where( and_( AlignmentRecord.truth_node_id == cid, AlignmentRecord.user_id == int(current_user.id), AlignmentRecord.tenant_id == tenant_id, ) ) ) row = row_result.scalar_one_or_none() if row is None: row = AlignmentRecord( truth_node_id=cid, user_id=int(current_user.id), alignment_score=body.score, is_confirmed=body.confirmed, comments=body.comments, tenant_id=tenant_id, ) db.add(row) else: row.alignment_score = body.score row.is_confirmed = body.confirmed row.comments = body.comments member_ids = await svc.get_project_member_ids(pid) confirmed_map: set[int] = set() rows_result = await db.execute( select(AlignmentRecord).where( and_( AlignmentRecord.truth_node_id == cid, AlignmentRecord.tenant_id == tenant_id, AlignmentRecord.is_confirmed.is_(True), ) ) ) for r in rows_result.scalars().all(): confirmed_map.add(int(r.user_id)) all_confirmed = set(member_ids).issubset(confirmed_map) if member_ids else False if all_confirmed: charter.status = "aligned" elif (charter.status or "").lower() not in {"aligned"}: charter.status = "aligning" if all_confirmed: content = _as_dict(charter.content) signal_ids = [sid for sid in content.get("signal_ids", []) if isinstance(sid, int)] if charter.signal_id and int(charter.signal_id) not in signal_ids: signal_ids.append(int(charter.signal_id)) if signal_ids: signal_rows = ( await db.execute( select(Signal).where( and_( Signal.tenant_id == tenant_id, Signal.id.in_(signal_ids), ) ) ) ).scalars().all() for signal in signal_rows: signal.status = "已关闭" signal.processed = True await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="alignment", entity_id=f"{cid}:{current_user.id}", action="confirm", after_data={ "score": body.score, "confirmed": body.confirmed, "all_confirmed": all_confirmed, "charter_status": charter.status, }, ) await db.commit() return success_response( { "charter_id": str(cid), "charter_status": charter.status, "all_confirmed": all_confirmed, "confirmed_count": len(confirmed_map), "member_count": len(member_ids), } )