257 lines
8.6 KiB
Python
257 lines
8.6 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)
|
|
|
|
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),
|
|
}
|
|
)
|
|
|