更新
This commit is contained in:
@@ -85,6 +85,18 @@ def _normalized_key(key: Any) -> str:
|
||||
return "".join(character for character in str(key).lower() if character.isalnum())
|
||||
|
||||
|
||||
def _appointment_id(value: Any) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise VideoTicketError("appointment_id must be a nonnegative integer") from exc
|
||||
if isinstance(value, bool) or parsed < 0 or str(value) != str(parsed):
|
||||
raise VideoTicketError("appointment_id must be a nonnegative integer")
|
||||
return parsed
|
||||
|
||||
|
||||
_FORBIDDEN_SECRET_KEYS = {"sdksecret", "sdksecretkey", "secretkey"}
|
||||
|
||||
|
||||
@@ -138,7 +150,8 @@ def _ticket_mapping(ticket: Any) -> Mapping[str, Any]:
|
||||
if hasattr(ticket, attribute_name)
|
||||
}
|
||||
if adapted:
|
||||
return adapted
|
||||
# Preserve server policy and identities; model attributes alone omit them.
|
||||
return {**(dict(raw) if isinstance(raw, Mapping) else {}), **adapted}
|
||||
raise VideoTicketError("backend ticket must be a mapping or call-ticket object")
|
||||
|
||||
|
||||
@@ -199,8 +212,15 @@ class VideoCallRequest:
|
||||
patient_id: Identifier | None = None
|
||||
call_record_id: Identifier | None = None
|
||||
backend_mode: BackendMode = BackendMode.EMBEDDED
|
||||
appointment_id: int = 0
|
||||
appointment_type: Any = None
|
||||
appointment_type_desc: str = ""
|
||||
can_video_call: bool = False
|
||||
can_audio_call: bool = False
|
||||
call_disabled_reason: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "appointment_id", _appointment_id(self.appointment_id))
|
||||
object.__setattr__(self, "sdk_app_id", _sdk_app_id(self.sdk_app_id))
|
||||
object.__setattr__(self, "user_id", _non_empty_string(self.user_id, "userID"))
|
||||
object.__setattr__(self, "user_sig", _non_empty_string(self.user_sig, "userSig"))
|
||||
@@ -257,6 +277,13 @@ class VideoCallRequest:
|
||||
"userSig": self.user_sig,
|
||||
"targetUserId": self.target_user_id,
|
||||
"diagnosisId": self.diagnosis_id,
|
||||
"patientId": self.patient_id,
|
||||
"appointmentId": self.appointment_id,
|
||||
"appointment_type": self.appointment_type,
|
||||
"appointment_type_desc": self.appointment_type_desc,
|
||||
"can_video_call": self.can_video_call is True,
|
||||
"can_audio_call": self.can_audio_call is True,
|
||||
"call_disabled_reason": self.call_disabled_reason,
|
||||
}
|
||||
|
||||
def safe_log_context(self) -> dict[str, Any]:
|
||||
@@ -340,6 +367,12 @@ def normalize_backend_ticket(
|
||||
diagnosis_id=normalized_diagnosis,
|
||||
patient_id=normalized_patient,
|
||||
call_record_id=payload_call_record,
|
||||
appointment_id=_appointment_id(payload.get("appointment_id", 0)),
|
||||
appointment_type=payload.get("appointment_type"),
|
||||
appointment_type_desc=str(payload.get("appointment_type_desc") or ""),
|
||||
can_video_call=payload.get("can_video_call") is True,
|
||||
can_audio_call=payload.get("can_audio_call") is True,
|
||||
call_disabled_reason=str(payload.get("call_disabled_reason") or ""),
|
||||
backend_mode=BackendMode.parse(backend_mode),
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from .launcher import VideoCallRequest
|
||||
from .launcher import VideoCallRequest, normalize_backend_ticket
|
||||
|
||||
ResultT = TypeVar("ResultT")
|
||||
|
||||
@@ -288,7 +288,29 @@ class OrderedCallLifecycle:
|
||||
with self._lock:
|
||||
return self.bound_room_id or self._claimed_room_id
|
||||
|
||||
def start(self) -> Future[bool]:
|
||||
def refresh_call_policy(self) -> Future[dict[str, Any]]:
|
||||
"""Fetch policy for this exact immutable consultation on the worker."""
|
||||
def operation() -> dict[str, Any]:
|
||||
ticket = self.repository.get_call_ticket(
|
||||
patient_id=self.request.patient_id,
|
||||
diagnosis_id=self.request.diagnosis_id,
|
||||
appointment_id=self.request.appointment_id,
|
||||
)
|
||||
refreshed = normalize_backend_ticket(
|
||||
ticket, patient_id=self.request.patient_id,
|
||||
diagnosis_id=self.request.diagnosis_id,
|
||||
)
|
||||
if (refreshed.appointment_id != self.request.appointment_id
|
||||
or refreshed.target_user_id != self.request.target_user_id
|
||||
or refreshed.user_id != self.request.user_id
|
||||
or refreshed.sdk_app_id != self.request.sdk_app_id):
|
||||
raise ValueError("当前问诊已变更,请重新打开聊天窗口")
|
||||
return refreshed.to_web_config()
|
||||
return self._worker.submit("refresh_policy", operation)
|
||||
|
||||
def start(self, *, call_type: int = 2) -> Future[bool]:
|
||||
if type(call_type) is not int or call_type not in (1, 2):
|
||||
raise ValueError("call_type must be 1 or 2")
|
||||
with self._lock:
|
||||
if self._start_future is not None:
|
||||
return self._start_future
|
||||
@@ -297,7 +319,8 @@ class OrderedCallLifecycle:
|
||||
raise ValueError("video repository does not implement start_call")
|
||||
payload: dict[str, Any] = {
|
||||
"diagnosis_id": self.request.diagnosis_id,
|
||||
"call_type": 2,
|
||||
"call_type": call_type,
|
||||
"appointment_id": self.request.appointment_id,
|
||||
}
|
||||
if self.request.patient_id is not None:
|
||||
payload["patient_id"] = self.request.patient_id
|
||||
|
||||
@@ -279,6 +279,7 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
call_ended = Signal(str) # type: ignore[misc]
|
||||
call_error = Signal(str) # type: ignore[misc]
|
||||
_start_completed = Signal(bool) # type: ignore[misc]
|
||||
_policy_completed = Signal(str, object) # type: ignore[misc]
|
||||
_room_completed = Signal(str, bool, str) # type: ignore[misc]
|
||||
_screenshot_completed = Signal(bool, str) # type: ignore[misc]
|
||||
_transcription_completed = Signal(str, str, str, bool, str) # type: ignore[misc]
|
||||
@@ -369,6 +370,7 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
self._connect_permissions()
|
||||
|
||||
self._start_completed.connect(self._on_lifecycle_started)
|
||||
self._policy_completed.connect(self._on_policy_completed)
|
||||
self._room_completed.connect(self._on_room_completed)
|
||||
self._screenshot_completed.connect(self._on_screenshot_completed)
|
||||
self._transcription_completed.connect(self._on_transcription_completed)
|
||||
@@ -509,7 +511,10 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
QTimer.singleShot(0, self._open_diagnosis_safely)
|
||||
return
|
||||
if event == "call-start-request":
|
||||
self._start_call_cycle()
|
||||
self._start_call_cycle(call_type=message.get("callType", 2))
|
||||
return
|
||||
if event == "call-policy-request":
|
||||
self._refresh_call_policy(str(message.get("requestId") or ""))
|
||||
return
|
||||
if event == "screenshot":
|
||||
self._save_screenshot(str(message.get("dataUrl") or ""))
|
||||
@@ -934,16 +939,48 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
"本次本地录音未正常结束,文件已保留以便排查。",
|
||||
)
|
||||
|
||||
def _start_call_cycle(self) -> None:
|
||||
if self._closing or self._start_requested:
|
||||
return
|
||||
def _prepare_call_cycle(self) -> None:
|
||||
if self._call_cycle_closed:
|
||||
self.lifecycle = self._lifecycle_factory()
|
||||
self._lifecycles.append(self.lifecycle)
|
||||
self._call_cycle_closed = False
|
||||
|
||||
def _refresh_call_policy(self, request_id: str) -> None:
|
||||
if not request_id or self._closing or self._shutdown_requested:
|
||||
return
|
||||
self._prepare_call_cycle()
|
||||
def completed(future: Future[dict[str, Any]]) -> None:
|
||||
try:
|
||||
policy = future.result()
|
||||
except Exception:
|
||||
policy = {"call_disabled_reason": "无法确认本次挂号通话权限,请重新打开聊天窗口"}
|
||||
with suppress(RuntimeError):
|
||||
self._policy_completed.emit(request_id, policy)
|
||||
try:
|
||||
self.lifecycle.refresh_call_policy().add_done_callback(completed)
|
||||
except Exception:
|
||||
self._on_policy_completed(request_id, {})
|
||||
|
||||
def _on_policy_completed(self, request_id: str, policy: dict[str, Any]) -> None:
|
||||
if self._closing or self._shutdown_requested or self._released:
|
||||
return
|
||||
# Send policy only; refreshed credentials never enter logs or notices.
|
||||
payload = {key: policy.get(key) for key in (
|
||||
"appointmentId", "appointment_type", "can_video_call", "can_audio_call",
|
||||
"call_disabled_reason", "appointment_type_desc",
|
||||
)}
|
||||
self._page.runJavaScript(
|
||||
"window.doctorConsultation?.callPolicyResult?.("
|
||||
+ json.dumps(request_id) + "," + json.dumps(payload, ensure_ascii=True) + ");"
|
||||
)
|
||||
|
||||
def _start_call_cycle(self, *, call_type: int = 2) -> None:
|
||||
if self._closing or self._shutdown_requested or self._start_requested:
|
||||
return
|
||||
self._prepare_call_cycle()
|
||||
self._start_requested = True
|
||||
try:
|
||||
future = self.lifecycle.start()
|
||||
future = self.lifecycle.start(call_type=call_type)
|
||||
except Exception:
|
||||
self._start_requested = False
|
||||
self._on_lifecycle_started(False)
|
||||
@@ -1149,8 +1186,8 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
self._closing = True
|
||||
self._media_active = False
|
||||
self._shutdown_timer.stop()
|
||||
if self._start_requested and not self._call_cycle_closed:
|
||||
self.lifecycle.end(self._close_reason)
|
||||
# Also retire policy-only workers for IM sessions without a live call.
|
||||
self.lifecycle.end(self._close_reason)
|
||||
self._abort_local_audio_recording("")
|
||||
self._release_webengine()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user