from __future__ import annotations from .shared import * from ...services.sstc.charter_data import _build_charter_summary_payload, _load_signal_rows, _select_source_signal, _serialize_alignment_record, _serialize_charter_signal, _serialize_charter_task, _serialize_source_signal from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project from ...services.sstc.feishu_routing import _backfill_feishu_sender_names from ...services.sstc.governance import _require_manager from ...services.sstc.signal_core import _as_dict, _parse_node_id, _parse_project_id, _sync_pmflow_rag_async router = APIRouter(tags=["sstc"]) @router.get("/projects/{project_id}/charters/{charter_id}") async def get_charter_detail( project_id: str, charter_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_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) content = _as_dict(charter.content) signal_ids = [sid for sid in content.get("signal_ids", []) if isinstance(sid, int)] signal_rows = await _load_signal_rows( db, tenant_id=tenant_id, signal_ids=signal_ids, ) if signal_rows: await _backfill_feishu_sender_names( db, tenant_id=tenant_id, signals=signal_rows, ) task_result = await db.execute( select(Task).where( and_( Task.truth_node_id == cid, Task.project_id == pid, Task.tenant_id == tenant_id, ) ) ) tasks = list(task_result.scalars().all()) align_result = await db.execute( select(AlignmentRecord).where( and_( AlignmentRecord.truth_node_id == cid, AlignmentRecord.tenant_id == tenant_id, ) ) ) alignments = list(align_result.scalars().all()) alignment_user_name_map: dict[int, str] = {} alignment_user_ids = sorted({int(item.user_id) for item in alignments if item.user_id is not None}) if alignment_user_ids: user_result = await db.execute( select(User.id, User.name, User.username).where( and_( User.tenant_id == tenant_id, User.id.in_(alignment_user_ids), ) ) ) for user_id, name, username in user_result.all(): uid = int(user_id) alignment_user_name_map[uid] = str(name or username or f"用户 {uid}") source_signal = _select_source_signal(charter, signal_rows) return success_response( { "id": str(charter.id), "title": charter.title, "content": content, "signal_ids": signal_ids, "signals": [_serialize_charter_signal(signal) for signal in signal_rows], "source_signal": _serialize_source_signal(source_signal) if source_signal else None, "tasks": [_serialize_charter_task(task) for task in tasks], "alignment_records": [ _serialize_alignment_record( record, alignment_user_name_map.get(int(record.user_id)), ) for record in alignments ], "status": charter.status, "version": charter.version, } ) @router.patch("/projects/{project_id}/charters/{charter_id}") async def update_charter( project_id: str, charter_id: str, body: CharterUpdateRequest, 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) before = { "title": charter.title, "content": _as_dict(charter.content), "version": charter.version, "status": charter.status, } aligned = (charter.status or "").lower() in ALIGNED_STATUSES if aligned and body.force_new_version: new_node = TruthNode( node_type="charter", parent_id=int(charter.id), project_id=pid, created_by=int(current_user.id), title=compact_title((body.title or charter.title), max_len=24), content=body.content if body.content is not None else _as_dict(charter.content), version=int(charter.version or 1) + 1, status="draft", tenant_id=tenant_id, ) db.add(new_node) await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="charter", entity_id=str(charter.id), action="new_version", before_data=before, after_data={"new_charter_id": "pending"}, ) await db.commit() await db.refresh(new_node) await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=pid) _sync_pmflow_rag_async( tenant_id=tenant_id, record_type="charter", record_id=int(new_node.id), project_id=pid, title=new_node.title, content=_as_dict(new_node.content), ) new_payload = _build_charter_summary_payload(new_node) new_payload["new_version_created"] = True return success_response(new_payload) if body.title is not None: charter.title = compact_title(body.title, max_len=24) if body.content is not None: charter.content = body.content 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(charter.id), action="update", before_data=before, after_data={ "title": charter.title, "content": _as_dict(charter.content), "version": charter.version, "status": charter.status, }, ) await db.commit() await db.refresh(charter) await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=pid) _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), ) update_payload = _build_charter_summary_payload(charter) update_payload["new_version_created"] = False return success_response(update_payload)