271 lines
9.5 KiB
Python
271 lines
9.5 KiB
Python
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)
|
||
charter_content = _as_dict(charter.content)
|
||
readiness = charter_content.get("readiness")
|
||
if not isinstance(readiness, dict):
|
||
readiness = {}
|
||
if body.confirmed and readiness.get("ready_for_confirm") is not True:
|
||
blocking_fields = [str(item or "").strip() for item in readiness.get("blocking_fields") or [] if str(item or "").strip()]
|
||
blocking_reasons = [str(item or "").strip() for item in readiness.get("blocking_reasons") or [] if str(item or "").strip()]
|
||
detail_parts = blocking_reasons + blocking_fields
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail="宪章未通过总确认校验" if not detail_parts else ";".join(detail_parts),
|
||
)
|
||
|
||
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)
|
||
if not member_ids:
|
||
# 无项目成员时至少以当前操作者作为最小确认集合,避免状态永久卡在“对齐中”。
|
||
member_ids = [int(current_user.id)]
|
||
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),
|
||
}
|
||
)
|